쿠버네티스에서 애플리케이션을 올리다 보면 이런 상황, 한 번쯤 겪습니다.
이런 “시작 전에 꼭 해야 하는 준비 작업”을 애플리케이션 코드에 섞어두면 점점 코드가 지저분해지고, 실패·재시작·롤백 정책이 꼬이기 시작합니다.
이 글에서는 이런 문제를 Pod 수준에서 깔끔하게 해결해주는 Init Container 패턴을 정리하고,
추가로 Kubernetes v1.28 ~ v1.34 기준 최신 기능(Native Sidecar, 컨테이너별 restartPolicy 등)까지 한 번에 묶어서 살펴봅니다.
Init Container 패턴은 "애플리케이션 컨테이너가 시작되기 전에 반드시 수행해야 하는 준비 작업을
별도의 초기화 컨테이너로 분리해 실행 순서를 보장하는 패턴”입니다.
Pod spec의 initContainers 섹션에 준비 작업을 담당하는 컨테이너들을 정의해두고,
이 컨테이너들이 모두 성공해야 비로소 containers 섹션의 메인 애플리케이션 컨테이너가 실행됩니다.
즉,
로 역할을 분리하는 패턴입니다.
클라우드 네이티브 애플리케이션은 흔히 “stateless”라고 부르지만,
실제로는 시작 시점에 여러 전제 조건이 필요합니다.
예를 들어
이런 것들을 애플리케이션 코드 안에서 처리하면
Init Container 패턴은 이런 책임을 Pod 레벨로 끌어올려서 정리해 주는 도구입니다.
기본 동작은 매우 단순합니다.
spec.initContainers 배열에 1개 이상 정의restartPolicy에 따라 재시작즉, Init 컨테이너는 “이 Pod가 Ready가 되기 전에 반드시 통과해야 하는 관문” 역할을 합니다.
따라서 Init Container에 들어가는 로직은 여러 번 실행돼도 안전한(idempotent) 형태로 만드는 것이 중요합니다.
Init 컨테이너도 일반 컨테이너와 거의 동일하게 설정할 수 있습니다.
등을 그대로 사용합니다.
보통 실행 시간이 짧기 때문에, 상황에 따라 리소스 요청을 작게 가져가거나,
반대로 “부트 단계에서만 세게 쓰는” 식으로 설계하는 것도 가능합니다.
Pod 안의 컨테이너들은 모두 동일한 네트워크 네임스페이스를 공유하고
정의된 Volume을 함께 사용할 수 있기 때문에, Init 컨테이너에서는 다음과 같은 패턴이 자연스럽습니다.
emptyDir / PVC / ConfigMap / Secret 같은 Volume에→ 메인 컨테이너는 “이미 파일/데이터가 준비된 상태”를 전제로 단순히 읽기만 하면 됩니다.
이 섹션에서는 실제 현업에서 자주 등장하는 케이스를 중심으로 Init 패턴을 정리합니다.
예시:
Init 컨테이너에서
메인 컨테이너 입장에서는 “이미 의존 서비스가 살아 있다”는 가정을 하고 단순하게 부팅할 수 있습니다.
예시
이렇게 하면 메인 컨테이너는 /config/app.yaml, /etc/ssl/certs/app.pem 같은 파일만 읽으면 되고 보안/자격증명 사용 로직은 Init 컨테이너에만 존재하게 되어 책임과 보안을 분리할 수 있습니다.
예시:
장점:
Init 컨테이너에서 DB 마이그레이션 스크립트를 실행하고 현재 스키마 버전을 체크하여, 필요할 때만 마이그레이션 수행 합니다.
다만, 여기에는 몇 가지 주의 사항이 있습니다.
예시:
이런 고권한 작업을 Init 컨테이너에서만 수행하고:
장점:
Init Container는 혼자 있는 기능이 아니라, Sidecar / Lifecycle Hook / Job 등과 함께 비교하면서 보는 게 이해가 쉽습니다.
공통점
차이점
정리하면:
postStart, preStop)즉, 복잡도와 책임이 커질수록 Lifecycle Hook 대신 Init 컨테이너로 옮기는 것이 자연스럽습니다.
둘은 대체제가 아니라 레벨이 다른 도구입니다.
Pod 재시작 → Init 전체 시퀀스 재실행
따라서 같은 작업을 여러 번 실행해도 문제가 없도록 설계해야 합니다.
Init 컨테이너는 짧게 실행되고 끝나버리기 때문에:
Pod spec에서 컨테이너별 securityContext를 분리해서 설정하면:
아래는 가장 단순한 형태의 Init 패턴 예시입니다.
외부 DB가 열려 있을 때까지 기다린 후에 앱을 기동하는 Pod 입니다.
apiVersion: v1
kind: Pod
metadata:
name: demo-init-container
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
echo "Checking DB availability..."
until nc -z mysql.example.com 3306; do
echo "DB not ready, sleeping..."
sleep 5
done
echo "DB is ready!"
containers:
- name: app
image: myorg/myapp:1.0.0
ports:
- containerPort: 8080
이 예제에서:
wait-for-db Init 컨테이너가 mysql.example.com:3306에 연결될 수 있을 때까지 반복 체크하고,app 시작이 섹션은 Kubernetes Patterns 책의 Init Container 장에서 잘 다루지 않는,
v1.28~v1.34 사이에 추가된 기능과 운영 관점의 보강 설명입니다.
기존에는 Pod 단위의 .spec.restartPolicy만 존재해서:
v1.34부터 컨테이너 단위 재시작 정책(알파)이 도입되면서:
Always, Init 컨테이너는 Never 등 서로 다른 정책을 갖도록 설정할 수 있습니다.이렇게 되면:
즉, v1.34 이후 Init 설계에서는 “컨테이너별 restartPolicy”까지 고려하는 것이 베스트 프랙티스에 가깝습니다.
1.28에서 알파로 도입된 SidecarContainers 기능은,
1.33 기준으로 안정화된 Native Sidecar 개념으로 정리되었습니다.
핵심 아이디어:
initContainers 배열에 있으면서도 restartPolicy: Always를 가진 컨테이너를 네이티브 Sidecar로 취급조합 예시:
restartPolicy: OnFailure(기본)책에서는 Init 컨테이너와 Sidecar 패턴을 완전히 분리된 개념으로 설명하지만,
실제 최신 쿠버네티스에서는 스펙 차원에서 Init과 Sidecar가 연결되어 있다는 점이 중요합니다.
책에서는 보통:
정도만 소개됩니다.
v1.34 즈음에는 이것이 발전해서:
운영 관점에서 이 패턴의 의미는:
즉, 책에서 아이디어 수준으로 언급되던 “Init → Volume → 메인” 패턴이 공식 구현/운영 가이드까지 포함하는 형태로 진화했습니다.
공식 문서에서는 Init 컨테이너 실패/디버깅을 위한 별도 가이드를 제공합니다.
대표적인 플로우는 다음과 같습니다.
kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <init-container-name>
kubectl run debug-init --rm -it --image=busybox:1.36 -- sh
kubectl debug로 동일 환경에 디버그 컨테이너 붙이기kubectl debug -it <pod-name> --image=busybox:1.36 --target=<init-container-name>
실제 운영 환경에서는 Init 실패가 배포 실패/롤아웃 지연의 주요 원인이 되는 경우가 많기 때문에,
Init 설계뿐 아니라 디버깅 루틴을 팀 차원에서 표준화해 두는 것이 중요합니다.
v1.28~v1.34 사이의 Init/Sidecar 관련 기능들은 대체로 다음과 같은 흐름을 가집니다.
Alpha 기능으로 들어와 기능 게이트 활성 + 실험적 사용이 필요Beta / GA(Stable) 단계로 가면서 기본 활성/비활성 상태와 동작 방식이 바뀔 수 있음따라서 클러스터를 운영할 때는
특히, 클러스터 업그레이드 시에는:
# 00-mysql.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "rootpassword" # 실습용 임시 값
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumes:
- name: data
emptyDir: {} # 실습용이니 영구 저장소는 생략
kubectl apply -f 00-mysql.yamlkubectl get po,svc;
kubectl logs -f pod/mysql-6b9fdfcd4d-lpk9h 

# 01-init-wait-for-db.yaml
apiVersion: v1
kind: Pod
metadata:
name: demo-init-wait-db
spec:
restartPolicy: Always
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
DB_HOST=${DB_HOST:-mysql}
DB_PORT=${DB_PORT:-3306}
echo "[init] waiting for DB at ${DB_HOST}:${DB_PORT}..."
until nc -z "${DB_HOST}" "${DB_PORT}"; do
echo "[init] DB not ready, sleep 5s"
sleep 5
done
echo "[init] DB is ready!"
env:
- name: DB_HOST
value: "mysql" # 여기! Service 이름
- name: DB_PORT
value: "3306"
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80
kubectl apply -f 01-init-wait-for-db.yamlkubectl get pod demo-init-wait-db -w
kubectl logs demo-init-wait-db -c wait-for-db
kubectl logs demo-init-wait-db -c app
kubectl describe po demo-init-wait-db 


Init이 Config API에서 설정을 가져와 파일로 저장하고,
앱 컨테이너는 해당 파일만 읽어 사용하는 패턴입니다.
# 02-init-generate-config.yaml
apiVersion: v1
kind: Pod
metadata:
name: demo-init-generate-config
spec:
restartPolicy: Never
volumes:
- name: config-volume
emptyDir: {}
initContainers:
- name: generate-config
image: busybox:1.36
volumeMounts:
- name: config-volume
mountPath: /config
command:
- sh
- -c
- |
echo "[init] generating config..."
cat <<EOF > /config/app.conf
APP_NAME=demo-app
LOG_LEVEL=INFO
BACKEND_URL=http://backend.default.svc.cluster.local
EOF
echo "[init] done."
containers:
- name: app
image: busybox:1.36
volumeMounts:
- name: config-volume
mountPath: /config
command:
- sh
- -c
- |
echo "[app] loaded config:"
cat /config/app.conf
echo "[app] sleeping..."
sleep 3600kubectl apply -f 02-init-generate-config.yamlkubectl get pod demo-init-generate-config -w
kubectl describe pod demo-init-generate-config
kubectl logs demo-init-generate-config -c generate-config
kubectl logs demo-init-generate-config -c app 


Kubernetes v1.33+에서 Native Sidecar 기능이 활성화된 클러스터라면,
initContainers 안에 restartPolicy: Always를 가진 컨테이너를 정의해 네이티브 사이드카로 동작시킬 수 있습니다.
# 03-init-with-sidecar.yaml
apiVersion: v1
kind: Pod
metadata:
name: demo-init-with-sidecar
spec:
restartPolicy: Always
volumes:
- name: shared-logs
emptyDir: {}
initContainers:
# 1) 한 번만 실행되는 전통 Init
- name: prepare-dir
image: busybox:1.36
volumeMounts:
- name: shared-logs
mountPath: /logs
command:
- sh
- -c
- |
echo "[init] preparing /logs dir..."
mkdir -p /logs
chmod 777 /logs
# 2) restartPolicy=Always 를 갖는 네이티브 사이드카
- name: log-shipper
image: busybox:1.36
restartPolicy: Always
volumeMounts:
- name: shared-logs
mountPath: /logs
command:
- sh
- -c
- |
echo "[sidecar] tail /logs/app.log"
touch /logs/app.log
tail -n+1 -F /logs/app.log
containers:
- name: app
image: busybox:1.36
volumeMounts:
- name: shared-logs
mountPath: /logs
command:
- sh
- -c
- |
i=0
while true; do
echo "[app] log line $i" | tee -a /logs/app.log
i=$((i+1))
sleep 5
done
kubectl apply -f 03-init-with-sidecar.yamlkubectl get pod demo-init-with-sidecar -w
kubectl logs demo-init-with-sidecar -c prepare-dir
kubectl logs demo-init-with-sidecar -c log-shipper
kubectl logs demo-init-with-sidecar -c app 

restartPolicy를 Init 컨테이너에 직접 주는 기능은
클러스터 버전 및 Feature Gate 상태에 따라 동작이 달라질 수 있습니다.
v1.33+ / 기본 설정 클러스터에서 테스트해 보는 것을 권장합니다.
Init:CrashLoopBackOff 같은 상태가 떴다고 가정하면,
가장 기본적인 디버깅 패턴은 다음과 같습니다.
# 1) 상태 및 이벤트 확인
kubectl describe pod <pod-name>
# 2) 특정 Init 컨테이너 로그 확인
kubectl logs <pod-name> -c <init-container-name>
# 3) 동일 명령을 별도 테스트 Pod에서 재현
kubectl run debug-init --rm -it --image=busybox:1.36 -- sh
# 4) 필요하면 kubectl debug 로 같은 Pod에 디버그 컨테이너 붙이기
kubectl debug -it <pod-name> --image=busybox:1.36 --target=<init-container-name>
이 흐름을 팀 내부 위키나 운영 매뉴얼로 정리해 두면, Init 관련 장애가 발생했을 때 공통 언어로 대응할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: demo-init-crashloop
spec:
restartPolicy: Always
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
echo "[app] running..."
sleep 3600
initContainers:
- name: init-fail
image: busybox:1.36
command:
- sh
- -c
- |
echo "[init-fail] starting..."
echo "[init-fail] I will fail now"
exit 1 kubectl apply -f 04-demo-init-crashloop.yamlkubectl get pod demo-init-crashloop -w
kubectl describe pod demo-init-crashloop 

kubectl debug -it demo-init-crashloop --image=busybox:1.36 --target=init-fail 
kubectl delete po demo-init-crashloop Init Container 패턴은 처음에는 “DB 기다리는 용도” 정도로만 보일 수 있지만, 실제로는 다음을 모두 포괄하는 강력한 도구입니다.
여기에 v1.28~v1.34 사이에 추가된 Native Sidecar, 컨테이너별 restartPolicy, Init 기반 Env/Config 주입 공식 패턴까지 함께 이해하면, 실제 프로덕션 환경에서 Init Container를 훨씬 더 안정적이고 유연하게 활용할 수 있습니다.
결국 핵심 메시지는 하나입니다.
“앱이 뜨기 전에 꼭 해야 하는 일을 앱 코드 밖으로 꺼내어 Pod 레벨에서 관리하자.”
복잡한 부트스트랩 로직이 애플리케이션 코드 안에 섞여 있다면,
한 번쯤 Init Container로 분리해 보는 리팩터링을 고려해볼 만합니다.
안녕하세요~ 오늘 블로그 글도 잘 봤습니다. 질문이 몇가지 있는데요~
1. Init Container가 CrashLoopBackOff 상태인데 로그도 짧고 금방 사라져서 디버깅이 어려운경우가 있었는데요. 보통 어떻게 디버깅 하시는지 궁금합니다.
2. 블로그에서 "Init Container로 권한 분리"를 언급했는데 구체적인 예시가 궁금합니다.