0. Contents
1. Overview
20장에서 만난 두 개의 벽
- 20장에서 ConfigMap과 Secret은 “설정 외부화의 표준 답안"이었다
- 그러나 마지막에 두 개의 벽을 만났다
- 1MB 크기 제한
- YAML 안에 YAML을 중첩하는 고통
Immutable Configuration 패턴: 벽 너머의 답
- Immutable Configuration 패턴은 그 벽 너머의 답이다
- 환경별 설정 데이터를 통째로 수동적(passive) 데이터 이미지에 넣는다
- 일반 컨테이너 이미지처럼 레지스트리로 배포하고 버전을 붙인다
- 런타임에 이 데이터 이미지와 애플리케이션 컨테이너를 연결해 설정을 꺼내 쓴다
이 장의 재미: “연결하는 방법”
- Docker에서는 한 줄로 끝나던 볼륨 연결이 Kubernetes에서는 왜 안 되는지
- 그리고 Init 컨테이너와 emptyDir로 어떻게 같은 결과를 만들어내는지가 핵심이다
2. Problem
2.1 Environment Variable과 ConfigMap이 멈추는 지점
19장과 20장이 해결한 것
- 19장의 환경 변수는 개수가 일정 수준을 넘는 순간 관리가 무너졌다
- 20장의 ConfigMap은 그 파편화를 한곳에 모아 해결했다
- 1.21부터는
immutable: true한 줄로 수정까지 막을 수 있게 됐다
- 1.21부터는
- 여기까지 오면 대부분의 설정 문제는 끝난다
- 그런데 현실의 설정은 계속 자란다
ConfigMap이 버거워지는 세 가지 순간
- ① 중첩 지옥
- WildFly 서버 설정처럼 XML/YAML을 YAML 안에 다시 넣어야 할 때
- 편집기 지원이 약해 들여쓰기 한 칸에 전체가 깨진다
- 저자들도 “여러 번 망쳤다(believe us!)“고 고백하는 부분
- ② 항목 폭발
- 앱이 설정 파일을 수십·수백 개 요구할 때
- ConfigMap 하나에 수백 개 키를 유지하는 것은 악몽이다
- ③ 크기의 벽
- 사전 학습된 ML 모델 같은 대용량 데이터
- etcd 기반 1MB 제한은 immutable로 잠가도 그대로다
- 아예 못 넣는다
완화할 수 있는 것과 없는 것
- ①·②는 도구로 어느 정도 완화된다
- ③은 근본적으로 넘을 수 없다
2.2 불변성이 뜻하는 것
불변성(immutability)의 정의
- 여기서 말하는 불변성(immutability) 은 “애플리케이션이 시작된 뒤에는 설정을 바꿀 수 없다"는 뜻이다
- 그래야 구성 데이터가 언제나 명확하게 정의된 상태(well-defined state)에 있다
- “지금 이 앱이 어떤 설정으로 도는가"에 확실히 답할 수 있다
가변 설정 vs 불변 설정
| 가변 설정 | 불변 설정 |
|---|---|
| 실행 중 누군가 슬쩍 수정 | 봉인. 바꾸려면 새 버전을 만들어 갈아 끼움 |
| “누가 언제 바꿨지?” 추적 불가 | Git에 올려 버전 관리 + 변경 관리 절차 적용 |
| 20장 3.7의 드리프트가 여기서 발생 | 누가·언제·왜 바꿨는지 기록이 남는 정식 작업 |
이 장의 목표 두 가지
- 불변이면서 버전 관리되는 설정
- 크기 제한이 없는 설정
3. Solution
3.1 첫 번째 선택지는 여전히 immutable ConfigMap
책이 먼저 못 박는 원칙
- 가장 간단하고 권장되는 방법은 immutable로 표시한 ConfigMap/Secret이다
- 설정이 1MB 안에 들어가고 YAML에 잘 담기면 그것으로 끝이다
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
demo.properties: |
greeting=hello
port=8080
immutable: true # 한 줄. 이후 kubectl edit은 거부되고, 변경은 새 이름으로만 (20장 3.8)
나머지 내용의 대상
- 이 장의 나머지는 이걸로 안 되는 경우를 위한 것이다
3.2 데이터 이미지 — 설정을 컨테이너 이미지로 굽는다
아이디어: 설정만 담긴 이미지
- 환경별 설정 파일들을 통째로 설정만 담긴 컨테이너 이미지로 만든다
- 앱 없이 파일만 들고 있어서 아무것도 실행하지 않는다
- 이 이미지를 수동적(passive) 데이터 이미지라 부른다
비유: 설정 파일만 담은 USB 메모리
- 개발용 USB, 운영용 USB를 따로 만들어둔다
- 앱을 켤 때 어떤 USB를 꽂느냐로 환경이 정해진다
- 크기 제한도 없다
어려운 단계: 시작 시점의 연결
- 이미지를 만드는 것은 쉽다
- 어려운 부분은 시작 시점에 앱과 연결하는 단계다
- 이것이 플랫폼마다 다르다
3.3 Docker Volume — 컨테이너가 볼륨을 소유한다
순수 Docker의 두 도구
- Kubernetes를 보기 전에 순수 Docker부터 본다
VOLUME지시어- 컨테이너 안의 디렉터리를 밖으로 노출한다
--volumes-from- 다른 컨테이너가 그 볼륨을 통째로 빌려 쓴다
# Example 21-1. Dockerfile-config — 설정 이미지
FROM scratch # ① 운영체제도 없는 완전히 빈 이미지
ADD app-dev.properties /config/app.properties # ② 개발용 설정 파일 하나를 넣는다
VOLUME /config # ③ "이 폴더는 나중에 다른 컨테이너가 가져다 쓸 수 있다" 표시
scratch를 쓰는 이유
- 할 일이 없기 때문이다
- 이 이미지는 파일 하나 든 껍데기다
- 프로그램이 단 하나도 없다
# Example 21-2. 설정 이미지 빌드 + 컨테이너 생성
docker build -t k8spatterns/config-dev-image:1.0.1 -f Dockerfile-config .
docker create --name config-dev k8spatterns/config-dev-image:1.0.1 # run이 아니라 create
# Example 21-3. 앱 실행 — 설정 컨테이너의 볼륨을 붙여서
docker run --volumes-from config-dev k8spatterns/welcome-servlet:1.0
create이지 run이 아닌 이유
- 실행할 프로그램이 애초에 없다
- 컨테이너를 만드는 목적은 딱 하나,
/config를 볼륨으로 등록해두는 것이다 - 그리고 이 순간에 중요한 일이 일어난다
docker create 순간, Docker가 자동으로 하는 두 가지
- 호스트 디스크에 실제 볼륨 폴더 생성
/var/lib/docker/volumes/xxx/_data
- 이미지 안 /config의 내용을 그 볼륨으로 복사
- VOLUME 규칙: 마운트가 원본을 가리므로 처음 한 번 채워준다
결과: config-dev “소유"의 볼륨
- 볼륨은 config-dev 컨테이너 “소유”
- config-dev 안 /config = 호스트 볼륨 폴더
공유의 조건: 컨테이너 밖으로 꺼내기
- 컨테이너끼리는 서로의 파일시스템이 안 보인다
- 다른 컴퓨터나 마찬가지다
- 그래서 config-dev 안에 박힌 파일을 앱이 직접 가져다 읽을 수는 없다
- Docker가 컨테이너 밖(호스트 볼륨)으로 꺼내놔야 공유가 가능하다
docker create가 그 꺼내는 작업을 대신 해준다
--volumes-from config-dev의 의미
- “config-dev가
VOLUME으로 표시해둔 폴더를 내 앱에도 같은 경로로 붙여줘"가 된다
운영 전환: 시작 명령만 교체
- 앱은 “내 설정은
/config에 있겠지"라고만 알고 있다 - 운영으로 옮길 때는 시작 명령만 바꾼다
# Example 21-4. 운영 환경 — 앱 이미지는 그대로, 설정 컨테이너만 교체
docker build -t k8spatterns/config-prod-image:1.0.1 -f Dockerfile-config .
docker create --name config-prod k8spatterns/config-prod-image:1.0.1
docker run --volumes-from config-prod k8spatterns/welcome-servlet:1.0
# ────────── 여기만 다르다
3.4 Kubernetes에는 컨테이너 볼륨이 없다
Kubernetes에 없는 것: 컨테이너 소유 볼륨
- Kubernetes에서 Pod 안 컨테이너끼리 볼륨을 공유하는 것은 원래 잘 된다
- 그런데 Docker의
--volumes-from처럼 “저 컨테이너 이미지 안에 든 디렉터리를 그대로 볼륨으로 쓰기” 는 없다 - Kubernetes 볼륨은 emptyDir, ConfigMap, PVC 같은 종류다
- “컨테이너 소유 볼륨"이라는 개념 자체가 없다
- Dockerfile의
VOLUME지시어에 대응하는 개념이 Kubernetes API에는 없다- 런타임이 컨테이너마다 익명 볼륨을 만들 수는 있다
- 하지만 다른 컨테이너와 공유할 수 없고 Kubernetes가 관리하지도 않는다
볼륨 소유 구조 비교
| Docker | Kubernetes | |
|---|---|---|
| 볼륨의 주인 | 컨테이너 (config-dev) | Pod |
| 컨테이너의 위치 | 다른 컨테이너가 “빌려 씀” | 컨테이너들은 전부 “이용자"일 뿐 |
| 이미지 안 폴더 | 이미지 안 폴더 = 볼륨 (직접 노출) | 이미지 안 폴더 ≠ 볼륨. 꺼내주는 사람 없음 |
지원 전망과 우회로
- 논의는 오래됐지만 구현 복잡도 대비 이점이 작아 가까운 시일 내 지원될 가능성은 낮다
- 책 기준 2023년, 실험적 CSI만 존재
- 이후 Image Volume 기능이 알파/베타로 들어오기 시작했다
- 그래서 우회로가 필요하다
3.5 Init Container — 복사로 우회하기
핵심: 방향을 뒤집는 것
- 아무도 남의 컨테이너 안을 들여다보지 않는다
- 대신 Pod 차원에서 빈 공유 폴더(emptyDir) 를 하나 만든다
- 15장의 Init 컨테이너가 앱보다 먼저 뜬다
- 자기 이미지 안의 파일을 그 폴더로 복사해놓고 퇴장한다
비유: 서랍과 복도 게시판
- Docker: 옆방 사람 서랍을 직접 열어 서류를 꺼내옴
- → Kubernetes에선 금지
- K8s: 옆방 사람이 먼저 와서 복도 게시판에 서류를 붙여놓고 감
- → 내가 나중에 게시판에서 읽음
Init 컨테이너를 쓰는 이유
- 일반 컨테이너(
containers)는 spec에 적힌 순서대로 하나씩 시작되지만, 앞 컨테이너가 준비될 때까지 기다리지 않는다 - 복사 컨테이너와 앱을 나란히 두면 앱이 먼저 떠서 빈 폴더를 볼 수 있다
- Init 컨테이너는 일반 컨테이너보다 먼저, 순서대로, 성공 종료할 때까지 실행된다
- “복사 끝남"이 보장된 상태에서 앱이 켜진다
emptyDir을 쓰는 이유
- 어차피 Pod가 시작할 때마다 Init이 새로 채우니 데이터가 남을 필요가 없다
- 원본은 이미지에 있으니 재시작 시 다시 복사하면 그만이다
- 오히려 임시 폴더라 “실행 중에 밖에서 손댈 수 없다"는 불변성이 자연스럽게 지켜진다
Docker 자동 복사 = Kubernetes 수동 복사
- Docker의
docker create자동 복사 = Kubernetes의 Init 컨테이너 수동 복사 - 결과는 같다
- 과정만 “직접 빌리기"에서 “중간에 복사"로 바뀐다
3.6 배달원의 Dockerfile 해부
보관함에서 배달원으로
- Docker 때의 이미지는 “보관함"이었다
- Kubernetes 때의 이미지는 “배달원"이어야 한다
- 그래서 Dockerfile이 달라진다
# Example 21-5. 개발 설정 이미지 (Kubernetes용)
FROM busybox # ① scratch 대신 busybox — cp와 sh가 필요하니까
ADD dev.properties /config-src/demo.properties # ② 원본은 /config가 아니라 /config-src에
ENTRYPOINT [ "sh", "-c", "cp /config-src/* $1", "--" ] # ③ 켜지면 복사하고 끝
Docker용과 Kubernetes용 Dockerfile 비교
| Docker (21-1) | Kubernetes (21-5) | 왜 | |
|---|---|---|---|
| 베이스 | scratch (완전 빈 것) | busybox (~1MB, cp/sh 포함) | 이제 “복사"라는 동작을 직접 해야 한다 |
| 마지막 줄 | VOLUME /config (선언) | ENTRYPOINT cp ... (명령) | 도커가 꺼내주던 일을 이미지가 스스로 |
| 파일 위치 | /config/ | /config-src/ | 아래 참고 |
/config-src인 이유: 마운트 그림자
- Kubernetes에서 emptyDir이
/config에 마운트되면 그 자리에 있던 이미지 원본은 빈 폴더에 가려진다- 20장 3.5의 실무 함정과 같은 리눅스 마운트 동작
- 원본을
/config에 뒀다면 복사할 것이 사라진다 - 그래서 원본은
src에 두고, 마운트된/config로 복사한다
ENTRYPOINT의 각 조각
줄별 역할과 비유
| 줄 | 역할 | 비유 |
|---|---|---|
FROM busybox | 도구 챙기기 | 배달원 채용 |
ADD | 설정 파일 싣기 | 짐 싣기 |
ENTRYPOINT | 도착지 받으면 내려놓고 퇴장 | 배달 지시서 |
빌드와 배포
- 이 이미지도 Docker 때와 마찬가지로
docker build+docker push로 레지스트리에 올린다 - 다만
docker create는 없다- 컨테이너를 띄우는 건 이제 Kubernetes가 한다
3.7 Deployment 명세 — 볼륨 하나, 컨테이너 둘
# Example 21-6. Init 컨테이너로 설정을 복사하는 Deployment (.template.spec 부분)
initContainers:
- image: k8spatterns/config-dev:1 # ① 방금 만든 배달원 이미지 — 환경 바꿀 땐 여기만
name: init
args:
- "/config" # ② ENTRYPOINT의 $1 → cp /config-src/* /config
volumeMounts:
- mountPath: "/config" # ③ 배달원이 짐 내려놓을 곳 = emptyDir
name: config-directory
containers:
- image: k8spatterns/demo:1 # ④ 앱 이미지 — dev든 prod든 절대 안 바뀜
name: demo
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: "/var/config" # ⑤ 같은 볼륨을 다른 경로에. 앱은 여기서 읽는다
name: config-directory
volumes:
- name: config-directory # ⑥ Pod 소유의 빈 공유 폴더
emptyDir: {}
volumes:의 위치: containers:와 같은 레벨
- YAML 구조가 그대로 소유 관계다
- 볼륨은 컨테이너 안에 있지 않고 Pod에 딸려 있다
- 컨테이너 쪽
volumeMounts:는 “Pod가 가진 볼륨 중 저것을 내 이 경로에 붙여줘"라는 연결 표시일 뿐이다 - 그래서 Init 컨테이너가 죽어도 볼륨은 남는다
- 볼륨은 Init의 것이 아니니까
마운트 경로: Init은 /config, 앱은 /var/config
- 같은 볼륨을 각자 원하는 위치에 건다
- 같은 USB를 A 컴퓨터는 D:에, B 컴퓨터는 E:에 꽂는 것과 같다
- 안에 든 파일은 하나다
마운트 시점: 항상 컨테이너 프로세스 시작 전
- Init이
cp를 실행하는 시점에 이미/config는 emptyDir이다 - 앱이 첫 줄을 실행하는 시점에 이미
/var/config/demo.properties가 보인다 - “폴더 없네” 할 틈이 없다
앱 쪽: 바뀌는 것 없음
demo:1은 그냥/var/config/demo.properties를 읽는 평범한 앱이다- 설정이 어디서 오는지, 누가 갖다 놓는지 모른다
- 이 패턴의 복잡함은 전부 “갖다 놓는” 쪽에 있다
- 앱은 로컬 개발 때처럼 파일을 읽을 뿐이다
- 그래서 앱 이미지 하나로 모든 환경을 돈다
3.8 환경 전환과 Template — 이미지 이름만 갈아 끼우기
개발 → 운영 전환: image 한 줄
- 개발 → 운영은 Init 컨테이너의
image한 줄이다 - YAML을 고쳐
kubectl apply하거나kubectl set image로 바꾸면 된다 - 그러나 환경이 다섯 개면 YAML 다섯 개를 따로 관리하게 된다
- 이는 실수의 온상이다
해법: 바뀌는 부분만 빈칸으로
- 해법은 바뀌는 부분(이미지 이름)만 빈칸으로 뚫어두는 것
- 책은 OpenShift Template을 예로 든다
# Example 21-7. OpenShift Template — 설정 이미지를 매개변수로
apiVersion: v1
kind: Template # ⚠️ 일반 Kubernetes에는 없는 kind. OpenShift 전용
metadata:
name: demo
parameters:
- name: CONFIG_IMAGE # ① 빈칸 선언
description: Name of configuration image
value: k8spatterns/config-dev:1 # 기본값 — 아무 말 없으면 dev
objects:
- apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
initContainers:
- name: init
image: ${CONFIG_IMAGE} # ② 빈칸 사용 — 예제 21-6과 다른 곳은 여기뿐
args: [ "/config" ]
volumeMounts:
- mountPath: /config
name: config-directory
containers:
- image: k8spatterns/demo:1
volumeMounts:
- mountPath: /var/config
name: config-directory
volumes:
- name: config-directory
emptyDir: {}
Template의 정체: YAML을 찍어내는 틀
- 이 파일 그대로는 Kubernetes에 못 먹인다
kind: Template은 Kubernetes가 모르는 종류다${CONFIG_IMAGE}는 아직 채워지지 않은 빈칸이다- Kubernetes는 변수 치환을 하지 않는다
- Template은 YAML을 찍어내는 틀이고, 찍어낸 결과물이 클러스터에 들어간다
# Example 21-8. 템플릿으로 앱 생성
oc new-app demo # 기본값 → dev
oc new-app demo -p CONFIG_IMAGE=k8spatterns/config-prod:1 # prod
매개변수의 범위
- 이 예제는
CONFIG_IMAGE하나만 뚫었다 - 실무에선 앱 이미지·레플리카 수·네임스페이스처럼 환경마다 달라지는 것들을 함께 매개변수로 뺀다
- 이 패턴의 맥락에서 핵심 변수는 CONFIG_IMAGE 하나다
- 이것만 바꾸면 환경이 바뀐다는 것을 보여주는 것이 목적이다
- 일반 Kubernetes에서는 Helm(
{{ .Values.configImage }})이나 Kustomize(patch)가 같은 역할을 한다
레퍼런스
"Init containers always run to completion. Each init container must complete successfully before the next one starts."
--volumes-from은 다른 컨테이너의 볼륨을 마운트하는 옵션으로, VOLUME 지시어로 선언된 경로가 컨테이너 생성 시 익명 볼륨으로 만들어지고 이미지의 해당 경로 내용으로 초기화된다는 동작이 3.3의 "docker create 순간 자동 복사"의 근거다.4. Discussion
책의 솔직한 평가
- 데이터 컨테이너를 쓰는 것은 솔직히 좀 번거롭다(admittedly a bit involved)
- 이미지를 굽고, 레지스트리에 올리고, Init 컨테이너를 짜야 한다
- immutable ConfigMap/Secret이 맞지 않을 때만 써야 한다
전체 메커니즘 정리
두 플랫폼 나란히 보기
- 같은 목표를 두 플랫폼이 어떻게 다르게 달성하는지 나란히 놓으면 이렇다
두 경로가 끝내 같아지는 지점
- 환경 전환
- Docker: –volumes-from 대상 이름 교체
- K8s: Init 컨테이너의 image 한 줄 교체 (템플릿 변수로)
- 앱 이미지
- 둘 다 그대로. 건드리지 않는다
- ★ 이 패턴의 존재 이유
4.1 데이터 컨테이너가 주는 것
① 봉인 + 버전
- 환경별 설정이 이미지 안에 봉인
- config-prod:1 → :2 처럼 앱 이미지와 같은 방식으로 버전 관리
- 문제 시 이전 태그로 롤백
② 레지스트리 배포
- Docker Hub 같은 곳에 올려두면 클러스터에 접속하지 않고도 docker pull로 “prod 설정 뭐였지?“를 로컬에서 확인
- ConfigMap은 클러스터 안에 들어가야 보인다
③ 진짜 불변
- 이미지는 원래 수정이 안 된다
- 바꾸려면 새 이미지·새 버전
- 몰래 손댈 수가 없다
④ 크기 무제한
- 1MB의 벽이 없다
- ML 모델 몇 GB도 담을 수 있다
4.2 대가와 판단 기준
① 복잡도
- 이미지 빌드·푸시·Init 작성
- 단계가 많다
② 보안 미해결
- 이미지 안에 DB 비밀번호를 넣으면 그 이미지를 받는 누구나 본다
- 이 패턴은 “안 바뀌게"이지 “안 보이게"가 아니다
- Secret은 별도
③ 복사는 우회책
- 진짜 원하는 건 “이미지를 볼륨으로 바로 꽂기"인데 그게 안 돼서 복사한다
- 파일이 크면 복사 시간과 디스크 두 배
- 이미지 볼륨이 정식화되면 없어질 오버헤드
④ Deployment 분기
- Init 이미지가 다르니 환경별 Deployment 객체가 갈라진다
- 템플릿으로 완화되지만 사라지진 않는다
설정의 모습별 도구 선택
| 설정의 모습 | 올바른 도구 |
|---|---|
| 작고 단순, 1MB 이하 | immutable ConfigMap/Secret (20장) ✅ |
| 크거나, 파일 수십 개거나, 레지스트리로 배포하고 싶음 | Immutable Configuration (이 장) |
| 크지만 환경마다 조금씩만 다름 | Configuration Template (22장) |
| 비밀번호·인증서 | Secret + 앱 수준 암호화 / 외부 비밀 관리 |
다음 장으로: Configuration Template
- “환경마다 조금씩만 다른 큰 설정 파일"이라면
- 이미지를 통째로 여러 개 만드는 대신
- 파일 하나에 빈칸을 뚫어 환경별로 채우는 쪽이 낫다
- 그것이 다음 장 Configuration Template이다
핵심 메시지
Immutable Configuration의 몫
- 1MB를 넘는 설정을 컨테이너 이미지로 버전 관리
연결 방법은 플랫폼이 정한다
- Docker → 설정 컨테이너가 볼륨을 소유, 앱이 –volumes-from으로 빌림
- Kubernetes → 컨테이너 볼륨이 없으므로 Init 컨테이너가 emptyDir로 복사해 앱에 넘김
Immutable Configuration은 “ConfigMap이 멈춘 자리에서 시작하는 답” 이다. 설정을 이미지로 구워 버전과 크기 문제를 한 번에 풀되, Kubernetes에는 이미지 안 폴더를 직접 꽂는 길이 없으니 Init 컨테이너라는 배달원이 emptyDir 게시판에 파일을 붙이고 퇴장하게 하고, 앱은 그 게시판만 읽게 하며, 환경 전환은 배달원 이미지 이름 한 줄로 끝내라. 그러나 이 모든 것은 번거로우니, immutable ConfigMap 한 줄로 되는지를 먼저 확인할 것.
