왜 앱 설정을 이미지에 구우면 안 되는가 — ConfigMap과 Secret
한 팀이 같은 앱을 개발/스테이징/운영 세 환경에 띄운다. 앱은 DB 호스트, API 키, 타임아웃 값을 설정 파일에서 읽는다. 처음엔 환경마다 이미지를 따로 빌드했다 — 설정 파일만 다른 이미지 세 개. 그러다 설정이 바뀔 때마다 빌드 3번, 푸시 3번, 배포 3번에 지쳤다. 그리고 깨달았다: 이미지는 코드여야지 환경 설정이어선 안 된다.
이 글이 푸는 것은: Kubernetes가 ConfigMap(비기밀 설정)과 Secret(기밀 값)으로 "설정을 이미지에서 빼내는" 방법, 그리고 Secret이 사실 "비밀"이 아니라는 진실이다.
설정은 세 가지로 주입된다
Kubernetes에서 컨테이너에 설정을 넣는 방법은 세 가지다. 모두 ConfigMap/Secret에서 온다.
- 환경 변수(
env): 컨테이너 시작 시 한 번 주입. 이후 갱신 안 됨. - 볼륨 마운트(
volumeMounts): 파일로 마운트. 갱신 시 실시간 반영(몇 분 내). - 명시적 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 같은 보조 도구가 실무에서 자주 쓰이는 이유.
생각해 볼 문제
- 환경 변수로 주입된 설정을 바꿨는데 앱에 반영이 안 된다. 왜인가, 어떻게 고치나?
- Secret을 RBAC 없이 누구나 읽을 수 있게 두면 어떤 위협이 생기나? etcd 암호화만으로 충분한가?
- 5MiB짜리 큰 설정을 ConfigMap 하나에 넣으려 한다. 실패한다. 해결책 세 가지.
- immutable ConfigMap을 쓰면 얻는 이득과 잃는 유연성을 trade-off로 설명하라.
- ConfigMap을 바꿨는데 Deployment Pod에 자동 반영되길 원한다. Kubernetes 코어만으로 가능한가?
- 볼륨 마운트한 ConfigMap 파일의 갱신이 수분 지연되는 이유는? (kubelet 동기화 주기 관점)
참고
- Kubernetes 공식 문서 - ConfigMaps - 접근 2026-07-12
- Kubernetes 공식 문서 - Secrets - 접근 2026-07-12 (base64, 한계)
- Kubernetes 공식 문서 - Secret의 보안 위험 - 접근 2026-07-12
- Kubernetes 공식 문서 - Projected Volume - 접근 2026-07-12
- Kubernetes 공식 문서 - Configure a Pod to Use a ConfigMap (볼륨 갱신) - 접근 2026-07-12
- KEP-大量 Immutable Secrets/ConfigMaps - 접근 2026-07-12
'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 |
