Chapter 7

컨피그맵과 시크릿: 애플리케이션 설정

  • 7.1 컨테이너 설정의 네 가지 방법
  • 7.2 커맨드와 인자 넘기기
  • 7.3 환경변수 설정
  • 7.4 ConfigMap으로 설정 분리
  • 7.5 Secret으로 민감 정보 전달

7장은 한 마디로 "앱 설정을 어떻게 컨테이너에 주입할까"에 대한 장이야. 이미지는 불변이어야 하는데, dev·staging·prod마다 설정은 달라야 하잖아. 답은 설정을 이미지에서 분리해서 쿠버네티스 쪽에서 주입하는 거야. 이 장에서 네 가지 주입 방식(command/args, 환경변수, ConfigMap, Secret)과 각각의 장단점을 다뤄.

설정을 주입하는 방법은 크게 커맨드라인 인자로 전달, 환경변수로 전달(하드코딩 또는 ConfigMap 참조), 볼륨으로 파일 마운트(ConfigMap 볼륨, Secret 볼륨)로 나뉘어. 짧은 값은 환경변수가 적합하고, 큰 설정 파일(Nginx conf, application.yml 같은 거)은 볼륨으로 주는 게 적합해. 어떤 걸 쓰든 핵심은 이미지 자체는 건드리지 않고 Pod 스펙에서 환경별로 다르게 주입한다는 거야.

먼저 커맨드와 인자. Dockerfile의 ENTRYPOINTCMD에 해당하는 쿠버네티스 필드는 각각 commandargs야. ENTRYPOINT가 command, CMD가 args. Pod spec에서 override할 수 있고, 대부분의 경우 command는 그대로 두고 args만 바꾸는 게 정석이야. 이미지가 ENTRYPOINT를 제대로 정의해뒀다면 그게 맞는 방식이지. 주의할 점은 commandargs는 Pod 생성 후 수정 불가라는 거. 바꾸려면 Pod를 다시 만들어야 해. 그리고 한 가지 중요한 Dockerfile 팁이 있어 — ENTRYPOINT는 반드시 exec 형식으로 써야 해. shell 형식이면 쉘이 PID 1로 떠서 시그널이 앱까지 전달되지 않거든.

환경변수는 컨테이너마다 리스트로 지정할 수 있어 (Pod 수준이 아니라 컨테이너 수준이야). env 아래에 name/value로 적어. 환경변수끼리 참조도 돼 — $(VAR) 문법으로. argscommand에서도 $(VAR) 참조가 가능해서, 환경변수에서 받은 값을 그대로 커맨드 인자로 넘길 수 있어. 문제는, 값이 YAML에 하드코딩되면 환경별로 YAML을 따로 관리해야 한다는 것이야. dev용 YAML, prod용 YAML 이런 식으로. 쿠버네티스의 철학은 "같은 Pod 정의를 여러 환경에 재사용"이니까 이 하드코딩을 풀어야 해. 답이 ConfigMap이야.

ConfigMap은 key-value 쌍을 담는 독립 객체야. Pod는 이 ConfigMap을 이름으로 참조하고, 같은 이름의 ConfigMap을 dev 네임스페이스엔 dev 값으로 prod 네임스페이스엔 prod 값으로 만들어두면 Pod YAML은 환경 상관없이 동일하게 쓸 수 있어. 앱 자체는 ConfigMap의 존재를 몰라도 돼. 쿠버네티스가 환경변수나 파일 형태로 컨테이너에 꽂아주기 때문에 앱은 평범하게 환경변수나 설정 파일을 읽으면 돼. 앱을 쿠버네티스-agnostic하게 유지하는 게 원칙이야.

ConfigMap을 만드는 방법은 세 가지야. 리터럴로 kubectl create configmap fortune-config --from-literal=sleep-interval=25처럼, 파일 하나를 --from-file=config-file.conf로, 디렉터리 전체를 --from-file=/path/to/dir로. 파일 기반으로 만들면 파일명이 key, 파일 내용이 value가 돼. 여러 옵션을 섞을 수도 있고. 컨테이너에 주입하는 방법도 여러 가지인데, 첫째는 환경변수로 특정 key 주입이야. env의 항목에서 valueFrom.configMapKeyRef로 ConfigMap 이름과 key를 지정해. 둘째는 환경변수로 전체 주입(envFrom)인데, ConfigMap의 모든 key가 환경변수로 노출돼. prefix는 선택이고. 주의할 점이, 환경변수 이름으로 유효하지 않은 key(예를 들면 FOO-BAR처럼 대시 포함)는 건너뛰어. 쿠버네티스가 자동 변환 안 해줘. 셋째는 커맨드라인 인자로 — 먼저 환경변수로 받고 args에서 $(VAR)로 참조하면 돼. 넷째는 볼륨으로 파일 마운트야. configMap 볼륨을 정의하고 volumeMounts로 컨테이너 경로에 붙이면, ConfigMap의 각 key가 해당 경로의 파일 하나로 노출돼. 큰 설정 파일을 주입할 때 이 방식이 정석이야.

여기서 함정이 하나 있어. ConfigMap 볼륨을 /etc/nginx/conf.d처럼 디렉터리에 마운트하면, 그 디렉터리에 원래 있던 파일들이 가려져. Nginx conf.d 같은 경우엔 문제 없지만, /etc 같은 공통 디렉터리에 마운트하면 시스템이 박살나. 해결책은 **subPath**야. 볼륨 전체가 아니라 특정 파일 하나만 기존 디렉터리에 꽂을 수 있어. volumeMountssubPath: myconfig.conf를 추가하면 돼. 다만 함정이 또 있는데, subPath로 마운트한 파일은 ConfigMap 업데이트 시 반영되지 않아. 파일 단위가 아니라 디렉터리 단위로 교체되기 때문이야. 이건 꽤 큰 함정이야.

ConfigMap 업데이트 자체는, 수정하면 이미 마운트된 볼륨의 파일도 자동 업데이트돼 (symlink 스왑으로 atomic하게). 다만 최대 1분 정도 걸릴 수 있고, 환경변수로 주입한 값은 업데이트되지 않아 — Pod 시작 시 한 번 꽂히고 끝이야. 그리고 앱이 파일 변경을 감지해서 reload해야 해. Nginx는 nginx -s reload 신호를 받아야 하는 식. 자동 재로드 안 해. subPath 마운트는 위에 말했듯이 업데이트가 안 되고. 그래서 현실적으로는 "ConfigMap 바꾸고 Pod 재시작"이 제일 안전한 경우가 많아. 실행 중인 Pod들이 서로 다른 시점에 업데이트되면 같은 이미지 기반인데 설정이 다른 상황이 생길 수 있거든. 이건 컨테이너 불변성의 원칙을 깨는 셈이야.

Secret은 ConfigMap과 거의 같은데 민감한 데이터용이야. 비밀번호, TLS 인증서·키, API 토큰 같은 거. ConfigMap과의 차이를 보면, base64 인코딩된 형태로 저장돼. 바이너리 데이터를 담을 수 있게 하기 위한 포맷일 뿐이지 암호화는 아니야 — 이걸 혼동하면 안 돼. etcd에 저장될 때 암호화되긴 해(쿠버네티스 1.7부터). 그 전엔 평문이었어. 그리고 Pod가 필요로 하는 노드에만 배포되고, 그 노드에선 디스크가 아니라 tmpfs(메모리)에 저장돼. 노드 디스크를 덤프해도 안 나와. 최대 크기는 1MB. base64가 귀찮으면 data 대신 stringData로 쓰면 평문으로 넣을 수 있어 (쿠버네티스가 자동 인코딩). 단 write-only — 읽을 땐 다시 data에 base64로 나와.

모든 Pod에는 default-token Secret이 자동으로 마운트돼. /var/run/secrets/kubernetes.io/serviceaccount/에 세 파일이 있어 — ca.crt(API 서버 검증용 CA 인증서), namespace(Pod가 속한 네임스페이스), token(API 서버 인증 토큰, 즉 Service Account 토큰). 이걸로 Pod 안의 앱이 API 서버에 인증할 수 있어. 8장과 12장에서 자세히 다뤄. 필요 없으면 automountServiceAccountToken: false로 끌 수 있고.

Secret을 환경변수로 노출하는 건 가능하지만 권장하지 않아. 이유는 두 가지야. 첫째 앱이 에러 로그에 환경변수 전체를 덤프하는 경우가 많아. 둘째 자식 프로세스가 부모의 환경변수를 전부 상속해서 서드파티 바이너리가 로그로 뱉을 수 있어. 그래서 Secret은 가능하면 볼륨으로 마운트하는 게 안전해. 파일은 tmpfs에 있고 접근이 필요한 프로세스만 읽으면 되니까. 프라이빗 레지스트리에서 이미지를 받아야 할 때도 Secret을 써. 타입이 docker-registry인 특수한 Secret을 만들고 Pod spec의 imagePullSecrets에 참조하면 Kubelet이 그 credential로 레지스트리에 로그인해서 이미지를 pull해. 매 Pod마다 이걸 참조하는 게 귀찮으면 ServiceAccount에 imagePullSecret을 붙여두면 그 SA를 쓰는 모든 Pod에 자동 적용돼 (12장에서 다뤄). 그리고 ConfigMap vs Secret 고를 때 원칙은 비민감 설정은 ConfigMap, 민감 데이터는 Secret. 설정 파일에 민감·비민감이 섞여 있으면 그 파일 전체를 Secret에 넣는 게 원칙이야.


정리

7장 읽고 기억할 거 세 가지:

  1. 이미지는 불변, 설정은 분리가 원칙이다. 설정을 Pod spec에 직접 박지 말고 ConfigMap/Secret으로 빼두면 같은 Pod 정의를 여러 환경에 재사용할 수 있다. 주입 경로는 네 가지 — 커맨드 인자, 환경변수 단건, 환경변수 전체(envFrom), 볼륨 파일. 짧은 값은 환경변수, 설정 파일은 볼륨이 정석.
  2. ConfigMap 볼륨의 함정 몇 가지를 기억해야 한다. 디렉터리 마운트는 기존 파일을 가린다 → subPath로 특정 파일만 꽂기. 하지만 subPath 마운트는 ConfigMap 업데이트 시 자동 반영 안 됨. 환경변수 주입도 업데이트 반영 안 됨. ConfigMap 수정 후에는 Pod 재시작이 제일 안전한 경우가 많다.
  3. Secret은 ConfigMap과 거의 같지만 민감 데이터용이다. base64 인코딩은 암호화가 아니라는 점, 가능하면 환경변수보다 볼륨 마운트(tmpfs에 저장)를 쓰는 점, 프라이빗 레지스트리 접근용 imagePullSecrets도 Secret의 한 종류라는 점. default-token Secret은 모든 Pod에 자동 마운트되어 API 서버 인증용으로 쓰인다.