0. Contents
1. Overview
21장과 이 장의 출발점
- 21장 Immutable Configuration은 “환경마다 설정 이미지를 통째로 하나씩” 만들었다
- 그런데 그 환경별 설정들을 나란히 놓고 보면 대부분 99%가 같고 몇 줄만 다르다
- 통째로 여러 벌 만드는 것은 낭비이자 위험이다
Configuration Template 패턴
- 설정 파일에서 환경마다 달라지는 자리만 빈칸으로 뚫어 템플릿으로 만든다
- 애플리케이션 시작 시점에 환경별 파라미터를 채워 그 환경 전용 완성본을 즉석에서 생성한다
이 장의 재미: 부품들의 역할 분담
- 템플릿은 어디에 두는지
- 빈칸에 넣을 값은 누가 어떤 경로로 배달하는지
- 완성본은 어떻게 앱 컨테이너 손에 들어가는지
- 조각이 많아 보이지만 하나씩 따라가면 전부 이미 아는 부품(Init 컨테이너, ConfigMap 볼륨, emptyDir)의 재조합이다
2. Problem
2.1 ConfigMap에 통째로 넣을 때 부딪히는 벽
20장 표준 답안의 한계
- 20장의 표준 답안대로 설정 파일을 ConfigMap에 직접 넣는 방식이 있다
- 설정이 크고 복잡해지는 순간 익숙한 두 개의 벽에 다시 부딪힌다
① 문법 충돌
- WildFly의 standalone.xml 같은 수백 줄짜리 XML을 YAML 안에 문자열로 중첩해야 한다
- 따옴표·들여쓰기·특수문자 하나에 리소스 정의 전체가 깨진다
- 21장 2.1의 “중첩 지옥” 재등장
② 크기의 벽
- ConfigMap/Secret 값의 총합은 1MB
- API 서버가 검증 단계에서 막는 고정 한도라 어떤 옵션으로도 못 넘는다
- etcd 자체의 요청 한도(기본 약 1.5MiB)와는 별개다
- 대형 설정 세트는 아예 안 들어간다
세 번째 문제의 등장
- 여기까지는 21장에서 본 것과 같은 벽이다
- 그런데 이 장은 세 번째 문제를 정면에 세운다
2.2 진짜 문제는 중복 — 998줄이 세 번 복사된다
원인: 환경 간 유사성
- 큰 설정 파일은 환경이 달라도 아주 조금만 다르다
- 이 유사성이 역설적으로 문제를 만든다
- 환경별로 ConfigMap을 따로 만들면 이렇게 된다
결과: 세 벌의 998줄과 드리프트
- 실제로 다른 것은 2줄뿐인데 998줄이 세 벌 존재한다
- 공통 부분을 고칠 일이 생기면 세 군데를 전부, 빠짐없이, 똑같이 고쳐야 한다
- 한 곳이라도 누락되는 순간 환경들의 설정이 미묘하게 어긋나기 시작한다
- 21장 2.2에서 본 드리프트가 바로 여기서 자란다
- “개발에선 되는데 운영에서만 터져요"류 버그의 온상이다
이 장의 목표
- 공통 부분을 딱 한 곳에만 존재하게 만들면서, 1MB 제한과 문법 충돌도 함께 피하는 것
3. Solution
3.1 발상 — 공통은 템플릿으로, 차이만 ConfigMap으로
비유: 결혼식 초대장 양식
- 결혼식 초대장 100장을 만들 때 100장을 전부 손으로 쓰는 사람은 없다
- 양식 한 장에 빈칸을 뚫고 이름만 바꿔 넣는다
- 이 패턴이 정확히 그것이다
저장 위치가 뒤집힌다
| 내용 | 두지 않는 곳 | 두는 곳 |
|---|---|---|
| 공통 998줄 | ConfigMap ❌ | 템플릿 파일로 만들어 “이미지"에 내장 ✅ |
| 다른 2줄 | 이미지 ❌ | 작은 값만 ConfigMap에 ✅ |
효과: 세 문제의 동시 해결
- ConfigMap에는 몇 줄짜리 값만 들어가니 1MB 제한과 YAML 문법 충돌을 회피한다
- 공통 부분은 템플릿 한 곳에만 존재하니 중복이 사라진다
남은 질문: 합치는 주체와 시점
- 흩어진 두 조각(템플릿 + 값)을 누가, 언제 합쳐서 앱이 읽을 완성본을 만드는가
3.2 빈칸은 언제 채우나 — ENTRYPOINT vs Init 컨테이너
합치는 시점: 애플리케이션 시작 직전
- 합치는 시점은 애플리케이션 시작 직전이다
- 앱이 켜지기 전에 완성본이 정해진 위치에 놓여 있어야 한다
- 앱은 그것이 템플릿이었는지도 모른 채 평범한 설정 파일로 읽는다
- 방법은 둘이다
방법 ①: ENTRYPOINT 방식
- Dockerfile의 ENTRYPOINT를 “템플릿 처리 → 앱 실행” 스크립트로 교체
- 파라미터는 환경 변수로 받는다
- ConfigMap → env 주입도 가능하나, 반드시 env를 “거친다”
- 단점: 앱 이미지를 직접 뜯어고쳐야 한다
방법 ②: Init 컨테이너 방식 ★ Kubernetes에서의 정답
- 템플릿 처리를 별도의 Init 컨테이너가 담당하고, 결과를 emptyDir로 앱에 넘긴다
- ConfigMap을 볼륨으로 마운트해 “파일 그대로, 직접” 읽는다
- 앱 이미지는 공식 이미지 순정 그대로
| ① ENTRYPOINT | ② Init 컨테이너 | |
|---|---|---|
| 처리 위치 | 앱 이미지 안의 스크립트 | 별도 준비 전용 컨테이너 |
| 앱 이미지 수정 | 필요 | 불필요 |
| 파라미터 경로 | ConfigMap → 환경 변수 → 스크립트 | ConfigMap → 볼륨(파일) → 처리기 |
| 파라미터 형태 | 단순 이름=값만 편함 | 여러 줄·복잡한 값도 파일이라 자유 |
책이 ②를 고르는 근거
- 책이 ②를 “가장 매력적(most appealing)“이라 하는 근거가 표에 다 있다
- ConfigMap을 directly(직접) 쓸 수 있고, 앱 이미지를 안 건드린다
- 21장에서 배달원(Init)과 게시판(emptyDir)을 이미 만들어봤다
- 이 장은 그 배달원에게 “복사” 대신 “빈칸 채우기“라는 더 똑똑한 일을 시키는 셈이다
3.3 빈칸의 실제 모습 — Go 템플릿과 Gomplate
예제: WildFly의 로그 접두어
- 예제는 WildFly 서버, 환경은 개발/운영 둘
- 차이는 단 하나 — 로그 줄 앞에
DEVELOPMENT:또는PRODUCTION:접두어가 붙는 것뿐이다 - 일부러 극단적으로 사소한 차이를 골랐다
- “이거 하나 때문에 설정 전체를 두 벌 관리할 거냐"를 보여주기 위해서
standalone.xml 속 빈칸 한 줄
- 수백 줄짜리 standalone.xml에서 빈칸은 이 한 줄이다
<!-- Example 22-1. 로그 설정 템플릿 (standalone.xml 중 일부) -->
<formatter name="COLOR-PATTERN">
<pattern-formatter pattern="{{(datasource "config").logFormat}}"/>
</formatter>
Gomplate: 빈칸을 채우는 도구
- 빈칸을 채우는 도구가 Gomplate(Go)다
- Tiller(Ruby) 같은 대안도 있지만 역할은 같다
- 템플릿과 값을 받아 빈칸이 채워진 완성본 파일을 만드는 것, 딱 그것뿐이다
- 우편물 자동 인쇄기라고 생각하면 된다
- 양식(템플릿)의 빈칸에 명단(ConfigMap)의 이름을 넣어 찍어낼 뿐, 편지 내용이 뭔지는 모른다
역할 분담: 해석은 Gomplate, 배달은 Kubernetes
- ConfigMap 안의 문자열을 “이건 YAML이고 logFormat 키의 값은 이거군"이라고 해석하는 것은 순전히 Gomplate다
- Kubernetes는 우체부처럼 내용을 읽지 않고 파일로 배달만 한다
3.4 두 줄짜리 Dockerfile — /in, /params, /out 규약
Init 컨테이너 이미지
- Init 컨테이너 이미지는 놀랄 만큼 간단하다
# Example 22-2. 템플릿 이미지의 Dockerfile
FROM k8spatterns/gomplate # ① 도구(Gomplate + 실행 스크립트)가 깔린 범용 베이스
COPY in /in # ② 개발자 PC의 in/ 폴더(템플릿)를 이미지 /in에 굽기
COPY in /in의 두 in
COPY in /in의 두in은 서로 다른 곳이다- 왼쪽은 빌드하는 개발자 PC의 폴더(Dockerfile 옆)
- 오른쪽은 이미지 안 경로
- 개발자가 작성한 standalone.xml 템플릿이 이 한 줄로 이미지에 박제된다
- 베이스가 도구를, COPY가 재료를 담당하니 두 줄로 끝난다
폴더 세 개 규약
- 베이스 이미지는 폴더 세 개로 일하는 규약을 갖고 있다
- 컨베이어 벨트다
- 📥 /in — 재료 투입구
- 템플릿
- 이미지에 내장 — 빌드 때 COPY로. 마운트 불필요!
- 📋 /params — 주문서 함
- Gomplate 데이터 소스인 YAML 파일
- ConfigMap 볼륨 마운트
- 📤 /out — 완성품 출구
- 처리된 설정 파일
- emptyDir — 앱 컨테이너와 공유
| 폴더 | 채워지는 시점 | 방법 | 왜 |
|---|---|---|---|
/in | 이미지 빌드 때 (미리) | Dockerfile COPY | 공통이라 모든 환경 동일 → 구워도 됨 |
/params | Pod 실행 때 (나중) | ConfigMap 볼륨 마운트 | 환경마다 다름 → 구우면 환경별 이미지가 됨 |
/out | Pod 실행 때 (나중) | emptyDir 마운트 | 다른 컨테이너와 공유해야 함 → 이미지 안 폴더론 불가 |
규약이 주는 재사용성
- 이 규약 덕에 베이스는 재사용된다
- WildFly든 Nginx든 템플릿만 갈아 끼우면(COPY 한 줄) 어떤 앱의 설정 생성기로든 변신한다
빌드 전후의 폴더 변화
- 빌드 전후로 폴더가 어떻게 바뀌는지 눈으로 따라가 보자
핵심: 텅 빈 채로 출하되는 두 폴더
/params와/out이 텅 빈 채로 출하된다는 것이 이 이미지의 핵심이다- 재료(템플릿)만 굽고 주문서와 완성품 자리는 비워두었다
- 그래서 같은 이미지가 개발에도 운영에도 쓰인다
3.5 ConfigMap — 환경마다 같은 이름, 다른 값
두 번째 재료
- 환경마다 달라지는 그 값의 실물이다
# Example 22-3. 템플릿에 채워 넣을 값을 담은 ConfigMap 생성 (개발 환경)
kubectl create configmap wildfly-cm \
--from-literal='config.yml=logFormat: "DEVELOPMENT: %-5p %s%e%n"'
2단계 구조: 키는 파일명, 값은 파일 내용
- 구조가 2단계라는 점을 놓치기 쉽다
- ConfigMap의 키가
config.yml이고, 그 값이 다시logFormat: ...이라는 YAML 한 줄이다 - 볼륨으로 마운트되면 키가 파일명, 값이 파일 내용이 되기 때문이다
.yml 확장자도 설계
- 키에
.yml확장자를 붙인 것도 설계다 - Gomplate가 확장자를 보고 “YAML로 파싱하면 되겠군"을 알아챈다
주의: 등록과 마운트의 차이
- 이 명령은 etcd에 리소스를 등록만 한다
- 아직 어떤 컨테이너와도 연결되지 않았고, 어디에도 파일로 보이지 않는다
- 파일이 되는 것은 Pod 정의에서 마운트를 선언하고 Pod가 실행되는 순간이다
- 유튜브에 영상을 올렸다고 TV에 나오지 않는 것과 같다
- 채널을 연결하고 전원을 켜야 한다
운영 환경의 ConfigMap
- 아까 질문의 답 — “어디 ConfigMap에 넣냐”
- 운영 클러스터에서는 같은 이름으로, 값만 바꿔 만든다
# 운영 환경 — 이름은 동일, 값만 PRODUCTION
kubectl create configmap wildfly-cm \
--from-literal='config.yml=logFormat: "PRODUCTION: %-5p %s%e%n"'
이름 참조로 정해지는 값
- Pod는
wildfly-cm이라는 이름으로만 참조한다 - 그래서 어느 클러스터에 배포되느냐에 따라 그 장소의 값이 자동으로 쓰인다
- 체인 호텔의 공통 안내문 양식과 지점별 금고
- 양식은 하나, 어느 지점에서 인쇄하느냐로 내용이 정해진다
등록 시점의 ConfigMap
- 이 시점의 ConfigMap은 아직 파일이 아니다
실행 시점에 채워지는 두 자리
- 실행 시점에 비어 있던 두 자리가 채워진다
- 아래는 Init 컨테이너 안에서 본 모습이다
마운트만으로 나온 완성본
- ②에서 텅 비어 있던
/params와/out이 마운트로 채워진 것이 보인다 - 이미지는 그대로인데 바깥에서 꽂아준 것만으로 개발용 완성본이 나왔다
- 운영 클러스터라면
/params/config.yml의 내용만PRODUCTION:으로 바뀌고 나머지는 전부 동일하다
3.6 Deployment 명세 — 볼륨 둘, 문패 셋
마지막 조각: 연결 설계도
- 모든 재료를 연결하는 설계도다
# Example 22-4. 템플릿 처리기를 Init 컨테이너로 가진 Deployment (.template.spec 부분)
initContainers:
- image: k8spatterns/example-config-cm-template-init # ① 22-2로 구운 이미지 (템플릿 내장)
name: init
volumeMounts:
- mountPath: "/params" # ② ConfigMap이 파일로 비치는 창구
name: wildfly-parameters
- mountPath: "/out" # ③ 완성본을 써낼 출구 = emptyDir
name: wildfly-config
containers:
- image: jboss/wildfly:10.1.0.Final # ④ 공식 이미지 순정 그대로 — 이 패턴의 자랑
name: server
command:
- "/opt/jboss/wildfly/bin/standalone.sh"
- "-Djboss.server.config.dir=/config" # ⑤ "설정은 /config에서 읽어" — 이미지 대신 옵션으로 방향만 튼다
volumeMounts:
- mountPath: "/config" # ⑥ ③과 같은 볼륨을 다른 문패로
name: wildfly-config
volumes:
- name: wildfly-parameters # ⑦ 볼륨의 "실체" 선언부 — ConfigMap과의 연결이 바로 여기
configMap:
name: wildfly-cm
- name: wildfly-config # ⑧ 빈 공유 폴더. Pod 소유, Pod과 함께 소멸
emptyDir: {}
연결은 두 단계로 나뉘어 적힌다
- 그래서 “ConfigMap 연결하는 데가 없는데?” 하고 놓치기 쉽다
volumes:(맨 아래)가 볼륨의 실체를 정한다- ⑦ “wildfly-parameters의 정체는 ConfigMap wildfly-cm”
- 각 컨테이너의
volumeMounts:가 그 볼륨을 자기 경로에 붙인다- ② “그걸 내 /params에”
- 채널 등록과 각 방 TV에서 채널 틀기가 따로인 것이다
- 21장 3.7에서 본 “볼륨의 주인은 Pod, 컨테이너는 이용자"가 여기서도 그대로다
같은 볼륨, 다른 문패
wildfly-config(emptyDir)는 실체가 하나인데 마운트가 두 번이다
복사도 전송도 없는 전달
- Init이 /out에 넣는 순간 그 파일은 이미 WildFly의 /config에 “도착"해 있다
- 복사도 전송도 없다 — 애초에 같은 방이니까
/in은 volumeMounts에 없다는 것도 확인하자- 템플릿은 3.4의
COPY로 이미지에 이미 들어 있으니, 실행 시점에 연결할 것이 없다
- 템플릿은 3.4의
3.7 실행 흐름 — “합치기"의 정체는 빈칸 치환 복사
Deployment 시작 후의 순서
- 이 Deployment를 시작하면 아래 순서로 움직인다
“합치기"의 정체: 치환 복사
- “합치기"는 두 파일을 이어 붙이는 것이 아니다
- 템플릿을 베껴 쓰다가 빈칸 자리만 ConfigMap 값으로 갈아 끼우는 치환 복사다
- 원본 템플릿(/in)은 그대로 남고, 새 완성본이 /out에 생긴다
- 대필 작가가 원고를 옮겨 적다가 “___“를 만나면 메모지를 보고 채워 넣는 것과 같다
emptyDir이 사라져도 되는 이유
- emptyDir이 Pod와 함께 소멸해도 문제없는 이유도 여기 있다
- 완성본은 템플릿+ConfigMap만 있으면 언제든 다시 구울 수 있는 빵이다
- Pod가 새로 만들어지거나 통째로 재시작될 때마다 Init이 새로 굽는다
- 덕분에 그 사이 ConfigMap이 바뀌었다면 최신 값으로 갱신되는 부수 효과까지 있다
- WildFly 컨테이너만 재시작될 때는 Init이 다시 돌지 않는다
- emptyDir은 컨테이너 재시작에도 남아 있어, 이미 구워 둔 완성본을 그대로 다시 읽는다
환경 전환: Deployment 무수정
- 이 Deployment는 한 글자도 안 바꾼다
- 21장(3.8)에선 그래도 Init 이미지 이름 한 줄은 바꿔야 했는데, 이 장에선 그마저 없다
- 이미지도 템플릿도 공통이고, 다른 것은 각 클러스터의 ConfigMap 값뿐이니까
3.8 디버깅 — 완성본을 눈으로 확인하기
어려움: 보이지 않는 중간 산출물
- 이 패턴은 부품이 많아 “안 될 때 어디를 봐야 하는지"가 즉각적이지 않다
- Init은 일 끝나면 사라지고, 완성본은 emptyDir 안이라 바로 안 보인다
- 책의 팁은 중간 산출물을 직접 열어보는 것이다
방법 ①: 노드에서 직접 (하드코어)
- emptyDir의 실체는 노드 디스크의 이 경로다
/var/lib/kubelet/pods/{podid}/volumes/kubernetes.io~empty-dir/
- 컨테이너가 죽어 있어도 볼 수 있다
- 대신 노드 접속 권한 필요
방법 ②: kubectl exec (간편) ⭐
kubectl exec -it <pod> -- cat /config/standalone.xml- 실행 중인 Pod에 들어가 마운트된 /config를 열어본다
증상별 문제 단계
- 완성본을 확인하면 문제 단계가 바로 좁혀진다
- 예: “로그에 DEVELOPMENT: 접두어가 안 붙어요”
| 완성본 상태 | 문제 단계 | 다음 확인 |
|---|---|---|
| /config에 파일이 없다 | Init 실패 | Init 로그를 보자 |
{{...}}가 그대로 남아 있다 | Gomplate가 값을 못 찾음 | ConfigMap 키 이름/데이터 소스 확인 |
| 파일도 값도 정상이다 | WildFly 쪽 | -Djboss...=/config 옵션 확인 |
레퍼런스
"A ConfigMap is not designed to hold large chunks of data. The data stored in a ConfigMap cannot exceed 1 MiB."
config.yml로 지은 이유가 이것이다.4. Discussion
4.1 DRY와 드리프트 차단
이 패턴이 사는 가치: DRY
- 이 패턴이 사는 가치는 DRY(Don’t Repeat Yourself — 반복하지 마라)다
| 패턴 적용 전 ❌ | 패턴 적용 후 ✅ | |
|---|---|---|
| 공통 998줄 | × 환경 수만큼 사본 | = 템플릿 “딱 한 곳” |
| 환경 전환 | ConfigMap 통째 교체 | 작은 값 몇 줄만 다름 |
| 공통 수정 | 모든 사본을 빠짐없이 (하나라도 놓치면 드리프트) | 템플릿 파일 하나 → 이미지 재빌드, 모든 환경에 동일 적용, 어긋날 사본 자체가 없음 |
사본은 어긋날 운명
- 책의 표현이 정확하다
- 통째 복사는 처음엔 작동하더라도(works initially) 시간이 지나며 어긋날 운명(doomed to diverge) 이다
- 사람이 여러 사본을 완벽하게 동기화하는 일은 언젠가 반드시 실패한다
- 템플릿 방식은 어긋날 사본을 없애 드리프트를 원천 차단한다
- Deployment·이미지·템플릿 전부가 환경 불변이고, 환경 의존적인 것은 ConfigMap의 작은 값들뿐이다
이웃 기술: OpenShift Template
- OpenShift에서는 리소스 정의(Deployment YAML) 자체를 파라미터화하는 OpenShift Template이 있다
- 21장 3.8에서 이미 사용
- 층이 다르다는 점만 구분하면 된다
- 이 장의 패턴은 앱 설정 파일의 빈칸을 채운다
- OpenShift Template(일반 K8s에선 Helm/Kustomize)은 매니페스트의 빈칸을 채운다
- 큰 설정 세트 문제는 후자가 풀어주지 못한다
- replicas 수처럼 매니페스트 수준의 환경 차이는 전자가 풀어주지 못한다
- 상황에 따라 둘을 함께 쓴다
4.2 대가와 판단 기준
책의 경고: 늘어난 움직이는 부품
- 책이 먼저 경고한다
- 이 설정은 더 복잡하고, 잘못될 수 있는 움직이는 부품(moving parts)이 더 많다
① 부품 수
- 템플릿 파일 + Go 템플릿 문법 + Init 이미지 빌드 + Gomplate + ConfigMap + 볼륨 둘 + 마운트 연결
- 기본 패턴은 ConfigMap 하나였다
② 고장 지점
- 템플릿 문법 오타, 데이터 소스 이름 불일치, 키 이름 오타, 마운트 경로 실수
- 어디서든 조용히 어긋날 수 있다
- 3.8의 디버깅이 필수 소양
③ 사용 조건
- “애플리케이션이 huge한 구성 데이터를 요구할 때만” 쓰라고 책이 못 박는다
판단 공식
- 판단 공식은 간단하다
- 설정 크기 = 큼 (수백 줄~) AND 환경별 차이 = 아주 작음 (몇 줄)
- → 이 조합일 때만 Configuration Template
- 예제가 딱 그랬다 — 수백 줄 중 로그 접두어 하나
| 설정의 모습 | 올바른 도구 |
|---|---|
| 작고 단순, 1MB 이하 | immutable ConfigMap/Secret (20·21장) ✅ |
| 크고, 환경마다 통째로 다르거나 레지스트리 배포가 필요 | Immutable Configuration (21장) |
| 크지만 환경마다 조금씩만 다름 | Configuration Template (이 장) ✅ |
| 매니페스트 자체가 환경별로 다름 (replicas 등) | OpenShift Template / Helm / Kustomize |
핵심 메시지
Configuration Template의 몫: “거대하지만 거의 같은” 설정을 사본 없이 관리
| 대상 | 처리 |
|---|---|
| 공통 | 빈칸 뚫린 템플릿으로 Init 이미지에 내장 (/in, COPY 한 줄) |
| 차이 | 환경마다 같은 이름의 ConfigMap에 작은 값만 (/params, 볼륨 마운트) |
| 합치기 | Init 컨테이너 안의 Gomplate가 시작 시점에 빈칸 치환 복사 |
| 전달 | /out = emptyDir = 앱의 /config. 같은 방, 다른 문패 |
| 앱 | 공식 이미지 순정 그대로. 완성된 파일을 “그냥 읽는다” |
| 환경 전환 | ConfigMap 값만 다르다. Deployment·이미지·템플릿은 한 글자도 안 바뀐다 |
Configuration Template은 “거의 같은 큰 설정을 여러 벌 복사하는 대신, 한 벌에 빈칸을 뚫는 답” 이다. 공통은 템플릿으로 Init 이미지에 굽고, 환경별 차이는 ConfigMap의 작은 값으로만 남겨, 시작 시점에 Gomplate가 빈칸을 채운 완성본을 emptyDir 게시판에 붙이고 퇴장하게 하라. 앱은 그 게시판을 평범한 설정 폴더로 읽을 뿐이고, 환경 전환은 ConfigMap 값 몇 줄로 끝난다. 그러나 부품이 많아 고장 지점도 많으니 — 설정이 정말 크고, 차이가 정말 작을 때만 꺼내 들 것.
