왜 앱 설정을 이미지에 구우면 안 되는가 — ConfigMap과 Secret

한 팀이 같은 앱을 개발/스테이징/운영 세 환경에 띄운다. 앱은 DB 호스트, API 키, 타임아웃 값을 설정 파일에서 읽는다. 처음엔 환경마다 이미지를 따로 빌드했다 — 설정 파일만 다른 이미지 세 개. 그러다 설정이 바뀔 때마다 빌드 3번, 푸시 3번, 배포 3번에 지쳤다. 그리고 깨달았다: 이미지는 코드여야지 환경 설정이어선 안 된다.

이 글이 푸는 것은: Kubernetes가 ConfigMap(비기밀 설정)과 Secret(기밀 값)으로 "설정을 이미지에서 빼내는" 방법, 그리고 Secret이 사실 "비밀"이 아니라는 진실이다.

설정은 세 가지로 주입된다

Kubernetes에서 컨테이너에 설정을 넣는 방법은 세 가지다. 모두 ConfigMap/Secret에서 온다.

  1. 환경 변수(env): 컨테이너 시작 시 한 번 주입. 이후 갱신 안 됨.
  2. 볼륨 마운트(volumeMounts): 파일로 마운트. 갱신 시 실시간 반영(몇 분 내).
  3. 명시적 API 호출: Pod가 apiserver에 ConfigMap/Secret을 조회. 가장 드묾.
# Kubernetes 1.36 — ConfigMap 만들기
apiVersion: v1
kind: ConfigMap
metadata: {name: app-config}
data:
  DB_HOST: db.internal
  TIMEOUT: "30"
  config.yaml: |
    log_level: info
    feature_x: true

이제 Pod에서 두 방식으로 사용:

spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    - name: DB_HOST                # 환경 변수로
      valueFrom: {configMapKeyRef: {name: app-config, key: DB_HOST}}
    volumeMounts:
    - name: cfg
      mountPath: /etc/app          # 파일로 마운트 (config.yaml이 /etc/app/config.yaml)
      readOnly: true
  volumes:
  - name: cfg
    configMap: {name: app-config}

환경 변수 방식의 함정: 컨테이너가 시작된 이후에 ConfigMap을 바꿔도 환경 변수는 안 바뀐다. 실시간 갱신이 필요하면 볼륨 마운트를 써야 한다. kubelet이 주기적으로 ConfigMap을 다시 읽어 마운트된 파일을 갱신한다(몇 분 주기). 앱이 파일 변경을 감지해 리로드하면 무중단 설정 변경이 가능하다.

ConfigMap의 크기 한계와Projected Volume

ConfigMap 하나의 한계는 1MiB(etcd 단일 객체 한계에서). 큰 설정은 쪼개야 한다. 여러 ConfigMap/Secret을 한 디렉터리에 합쳐 마운트하려면 projected volume을 쓴다:

volumes:
- name: combined
  projected:
    sources:
    - configMap: {name: app-config}
    - secret:    {name: app-secret}
    - downwardAPI: {}

한 볼륨에 여러 소스를 합쳐 마운트 — 설정과 비밀을 한 곳에서.

Secret — ConfigMap과 거의 같은데, "비밀"은 아니다

Secret은 ConfigMap과 API상으로 거의 같다. 한 가지 차이: 값을 data 대신 base64로 저장한다. 그리고 기본적으로 kubelet이 노드에 전달하고, 노드(tmpfs)에만 올린다.

apiVersion: v1
kind: Secret
metadata: {name: api-key}
type: Opaque
data:
  API_KEY: c2VjcmV0LXZhbHVl             # base64("secret-value")
# 또는 stringData로 평문 입력
# stringData:
#   API_KEY: secret-value

하지만 여기서 진실 하나: Secret은 기본적으로 "비밀"이 아니다.

  • 값이 base64로 저장될 뿐, 암호화가 아니다. echo c2VjcmV0LXZhbHVl | base64 -d 한 번이면 원문이 나온다.
  • etcd에 평문(base64)으로 저장된다. RBAC로 Secret 읽기 권한이 있으면 그냥 본다.
  • 컨테이너의 환경 변수로 주입된 Secret은 kubectl exec ... env로 노출된다.

진짜 "비밀"로 만들려면 etcd 저장 암호화(encryption at rest)를 켜야 한다 — apiserver가 etcd에 쓰기 전에 암호화. 그리고 외부 비밀 관리(External Secrets Operator, Vault)로 Secret을 클러스터 밖에서 주입하는 것이 실무 정석이다(03-k8s-security 영역에서 깊이).

immutable ConfigMap/Secret — 갱신 안 되는 설정

설정이 절대 안 바뀐다고 확신하면 immutable: true를 준다. 이렇게 하면:

  • kubelet이 주기적 폴링을 안 해 apiserver 부하가 줄어든다.
  • 실수로 수정하려 하면 API가 거부한다.

대규모 클러스터에서 ConfigMap/Secret이 수천 개면 immutable로 폴링 비용을 크게 줄일 수 있다. 단, 바꾸려면 새 객체를 만들고 참조를 바꿔야 한다.

ConfigMap/Secret 갱신과 롤아웃의 관계

볼륨 마운트 방식은 ConfigMap 갱신이 몇 분 내 파일에 반영된다. 하지만 환경 변수 방식은 반영이 안 되므로 Pod 재시작이 필요하다. Deployment의 경우 ConfigMap 변경이 자동 롤아웃을 유발하지 않는다 — 수동으로 kubectl rollout restart를 해야 한다.

이것이 "Reloader" 같은 도구가 널리 쓰이는 이유다 — ConfigMap/Secret이 바뀌면 연관 Deployment를 자동 롤아웃. (Kubernetes 코어엔 이 기능이 없다.)

직접 확인하기

# Kubernetes 1.36 — ConfigMap 만들고 환경 변수 + 파일로 주입
kubectl create configmap cfg --from-literal=GREETING=hello --from-literal=COUNT=3
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: cfg-test}
spec:
  containers:
  - name: app
    image: alpine:3.20
    command: ["sh","-c","env | grep GREETING; cat /cfg/COUNT; sleep 3600"]
    env: [{name: GREETING, valueFrom: {configMapKeyRef: {name: cfg, key: GREETING}}}]
    volumeMounts: [{name: c, mountPath: /cfg, readOnly: true}]
  volumes: [{name: c, configMap: {name: cfg}}]
EOF
kubectl logs cfg-test

확인할 것: 환경 변수 GREETING=hello와 파일 /cfg/COUNT(내용 "3") 둘 다 보인다.

GREETING=hello
3
# Secret의 base64 "보호"가 얼마나 허술한지 확인
kubectl create secret generic demo --from-literal=pw=topsecret
kubectl get secret demo -o jsonpath='{.data.pw}' | base64 -d

확인할 것: topsecret이 그대로 복구된다 — base64는 인코딩이지 암호가 아니다.

topsecret

envFrom vs env — 일괄 주입 vs 개별 주입

ConfigMap/Secret을 환경 변수로 주입하는 두 방식:

  • env(개별): 키 하나씩 명시.
  • envFrom(일괄): ConfigMap/Secret의 모든 키를 환경 변수로 한 번에.
envFrom:
- configMapRef: {name: app-config}     # 모든 키가 환경 변수로

envFrom은 편하지만 함정: ConfigMap에 유효하지 않은 환경 변수 이름(my.key, 숫자 시작 등)이 있으면 Pod 시작이 실패. prefix(prefix:)로 이름을 정규화하거나, 안전하려면 env로 개별 선택.

subPath와 볼륨 마운트의 충돌 — 디렉터리 덮어쓰기 함정

ConfigMap/Secret을 특정 파일*만 마운트하려 할 때의 함정. 볼륨을 /etc/app에 마운트하면 — 그 디렉터리의 *기존 파일들이 숨겨짐(볼륨이 덮어씀). 컨테이너 이미지의 /etc/app/에 있던 파일이 ConfigMap 마운트로 안 보이게.

subPath가 이것을 해결 — 볼륨의 특정 파일만 기존 디렉터리에 겹쳐 마운트:

volumeMounts:
- name: cfg
  mountPath: /etc/app/config.yaml     # 특정 파일
  subPath: config.yaml                 # 볼륨 중 이 파일만

이제 /etc/app/config.yaml만 ConfigMap으로 대체되고, 같은 디렉터리의 다른 파일은 이미지 원본 유지. "전체 디렉터리가 날아갔다"는 함정의 해결.

downwardAPI — Pod 자신의 메타데이터 주입

ConfigMap이 아닌 Pod 자신의 정보(이름, IP, 라벨, 자원 한계)를 환경 변수/파일로 주입하는 downwardAPI:

env:
- name: POD_NAME
  valueFrom: {fieldRef: {fieldPath: metadata.name}}
- name: POD_IP
  valueFrom: {fieldRef: {fieldPath: status.podIP}}

앱이 "내 이름/IP/라벨"을 알아야 할 때(예: 클러스터 안 자기 식별). ConfigMap 없이 Pod 메타데이터를 앱에. 서비스 디스커버리·메트릭 태깅에 유용.

immutable의 트레이드오프 — 유연성 vs 안정성

immutable: true가 폴링 비용을 줄이지만 유연성을 희생:

  • 장: kubelet 폴링 안 함, apiserver 부하 감소. 대규모에서 유의미.
  • 단: 설정 변경 시 새 객체를 만들어야(기존 수정 불가). GitOps/버전 관리에는 오히려 자연스럽지만, 임시 수정이 번거로움.

이 트레이드오프가 "항상 immutable이 좋다"가 아닌 이유. 자주 바뀌는 설정은 mutable(마운트 갱신)이 편하고, 거의 안 바뀌는 설정은 immutable이 부하 절감. 설정의 변경 빈도가 선택 기준.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"Secret은 암호화돼 저장된다" 기본은 base64 인코딩일 뿐. etcd 저장 암호화를 켜야 진짜 암호
"ConfigMap을 바꾸면 Pod에 즉시 반영된다" 볼륨 마운트는 수분 지연, 환경 변수는 재시작까지 반영 안 됨
"Secret은 환경 변수보다 안전하다" 환경 변수로 주입된 Secret도 env로 노출. 볼륨 파일이 약간 더 나음
"ConfigMap은 무한히 클 수 있다" 1MiB 한계. 큰 건 쪼개거나 projected volume
"immutable ConfigMap은 갱신이 안 되서 위험" 의도된 안전장치. 바꾸려면 새 객체로 교체 — 설정 드리프트 방지
"ConfigMap 변경이 Deployment 자동 롤아웃을 유발한다" 유발 안 함. 수동 restart 필요(또는 Reloader 도구)

요약 — 이 글의 결론

  • 설정은 이미지에서 빼야 한다. 환경마다 이미지를 따로 빌드하면 빌드/배포 비용이 환경 수 배로 커진다. ConfigMap/Secret으로 설정을 런타임에 주입.
  • 두 주입 방식의 차이: 환경 변수(시작 시 고정) vs 볼륨 마운트(수분 지연 갱신). 실시간 갱신이 필요하면 마운트.
  • Secret은 기본이 "비밀"이 아니다. base64 인코딩일 뿐, etcd엔 사실상 평문. 진짜 보호는 etcd 암호화 + 외부 비밀 관리(Vault/External Secrets).
  • immutable ConfigMap/Secret이 대규모 폴링 비용을 줄인다. 바꾸려면 새 객체로 교체.
  • ConfigMap/Secret 변경은 자동 롤아웃을 안 유발한다. 환경 변수 주입 Pod는 재시작까지 안 바뀐다. Reloader 같은 보조 도구가 실무에서 자주 쓰이는 이유.

생각해 볼 문제

  1. 환경 변수로 주입된 설정을 바꿨는데 앱에 반영이 안 된다. 왜인가, 어떻게 고치나?
  2. Secret을 RBAC 없이 누구나 읽을 수 있게 두면 어떤 위협이 생기나? etcd 암호화만으로 충분한가?
  3. 5MiB짜리 큰 설정을 ConfigMap 하나에 넣으려 한다. 실패한다. 해결책 세 가지.
  4. immutable ConfigMap을 쓰면 얻는 이득과 잃는 유연성을 trade-off로 설명하라.
  5. ConfigMap을 바꿨는데 Deployment Pod에 자동 반영되길 원한다. Kubernetes 코어만으로 가능한가?
  6. 볼륨 마운트한 ConfigMap 파일의 갱신이 수분 지연되는 이유는? (kubelet 동기화 주기 관점)

참고

'Tech Artifacts > Kubernetes' 카테고리의 다른 글

Kubernetes - 12. autoscaling  (0) 2026.07.13
Kubernetes - 11. resource qos  (0) 2026.07.13
Kubernetes - 09. storage  (0) 2026.07.13
Kubernetes - 08. scheduling  (0) 2026.07.13
Kubernetes - 07. daemonset job cronjob  (0) 2026.07.13