Deployment를 배포했는데 Pod가 Running 상태를 유지하지 못합니다.
kubectl get pods를 치면 RESTARTS가 계속 올라가고, STATUS가CrashLoopBackOff입니다. 로그를 봐도 뭐가 문제인지 바로 안 보입니다. CrashLoopBackOff는 "컨테이너가 시작 직후 종료를 반복하고 있다"는 신호이고, Exit Code와 로그를 함께 봐야 원인을 좁힐 수 있습니다.
핵심 요약
| 원인 | Exit Code / 키워드 | 확인 방법 |
|---|---|---|
| 코드 버그 / 미처리 예외 | Exit 1, Exception 메시지 | kubectl logs --previous |
| 환경변수 / 설정 누락 | Exit 1, config 관련 에러 | ConfigMap, Secret 마운트 확인 |
| OOMKilled | Exit 137, OOMKilling 이벤트 | kubectl describe pod Events |
| Command / Entrypoint 오류 | Exit 126/127 | Dockerfile ENTRYPOINT, Pod command 확인 |
| Liveness Probe 실패 | Liveness probe failed 이벤트 | Probe 설정값과 앱 시작 시간 비교 |
| 의존 서비스 연결 실패 | Exit 1, Connection refused | 의존 서비스 상태, NetworkPolicy 확인 |
| Volume 권한 문제 | Permission denied | securityContext, fsGroup 확인 |
1. CrashLoopBackOff란
CrashLoopBackOff는 Pod 상태(status)가 아니라, 컨테이너가 반복적으로 실패하고 있을 때 Kubernetes가 재시작 간격을 점진적으로 늘리는 동작을 나타냅니다.
컨테이너가 시작 직후 비정상 종료하면, kubelet은 restartPolicy에 따라 컨테이너를 재시작합니다. 재시작할 때마다 대기 시간을 10초 → 20초 → 40초 → ... 최대 5분까지 늘립니다(exponential backoff). 이 대기 상태가 CrashLoopBackOff입니다.
핵심 포인트:
- CrashLoopBackOff 자체는 "재시작 대기 중"이라는 의미입니다. 실제 원인은 컨테이너 종료 사유에 있습니다.
- 컨테이너가 10분 이상 정상 실행되면 backoff 타이머가 초기화됩니다.
restartPolicy: Never인 경우 CrashLoopBackOff는 발생하지 않습니다. 대신 Pod 상태가Failed로 고정됩니다.
2. 진단 시작: 3가지 확인 포인트
CrashLoopBackOff가 발생하면 아래 세 가지를 순서대로 확인합니다.
2-1. Exit Code 확인
kubectl describe pod <pod-name> -n <namespace>
출력에서 확인할 부분:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Thu, 05 Jun 2026 10:30:00 +0900
Finished: Thu, 05 Jun 2026 10:30:02 +0900
Exit Code가 원인을 좁히는 가장 빠른 단서입니다.
| Exit Code | 의미 | 대표 원인 |
|---|---|---|
| 0 | 정상 종료 | 프로세스가 의도대로 끝남 (데몬이 아닌 프로그램) |
| 1 | 일반 에러 | 코드 버그, 설정 파일 누락, 의존 서비스 연결 실패 |
| 126 | 명령어 실행 불가 | 실행 파일 권한 없음 (chmod +x 필요) |
| 127 | 명령어 없음 | Entrypoint/Command 경로 오류, 바이너리 누락 |
| 137 | SIGKILL (9) | OOMKilled 또는 외부에서 강제 종료 |
| 139 | SIGSEGV (11) | Segmentation fault, 메모리 접근 위반 |
| 143 | SIGTERM (15) | 정상 종료 요청 (preStop hook, 롤링 업데이트) |
Exit Code 0인데 CrashLoopBackOff?
컨테이너의 메인 프로세스가 정상 종료(Exit 0)해도, Kubernetes는 restartPolicy: Always(Deployment 기본값)에 따라 컨테이너를 다시 시작합니다. 이 패턴은 일회성 작업을 Deployment로 실행했을 때 발생합니다. 배치 작업은 Job이나 CronJob을 사용해야 합니다.
2-2. 이전 컨테이너 로그 확인
CrashLoopBackOff 상태에서 kubectl logs를 실행하면 "현재 재시작 중인" 컨테이너의 로그가 나옵니다. 이미 종료된 이전 컨테이너의 로그를 봐야 실제 에러를 확인할 수 있습니다.
# 이전(종료된) 컨테이너 로그 확인
kubectl logs <pod-name> --previous -n <namespace>
# 멀티 컨테이너 Pod인 경우 컨테이너 지정
kubectl logs <pod-name> -c <container-name> --previous -n <namespace>
# 최근 50줄만 확인
kubectl logs <pod-name> --previous --tail=50 -n <namespace>
--previous 플래그는 직전 1회 종료된 컨테이너의 로그만 보여줍니다. Pod가 여러 번 재시작된 경우 이전 로그는 유실됩니다. 로그 수집 시스템(Fluentd, Fluent Bit, CloudWatch Logs 등)이 있다면 그쪽에서 전체 이력을 확인하는 것이 더 정확합니다.2-3. Events 확인
kubectl describe pod <pod-name> -n <namespace> | grep -A 20 "Events"
Events에서 확인할 키워드:
| 이벤트 메시지 | 의미 |
|---|---|
Liveness probe failed |
Liveness Probe 응답 실패로 컨테이너 재시작 |
OOMKilling |
메모리 limit 초과로 커널이 프로세스 종료 |
PostStartHook failed |
postStart lifecycle hook 실행 실패 |
Back-off restarting failed container |
CrashLoopBackOff 진입 확인 |
FailedMount |
Volume 마운트 실패 |
3. 원인별 분석
3-1. 코드 버그 / 미처리 예외
가장 흔한 원인입니다. 애플리케이션이 시작 직후 예외를 던지거나 패닉 상태로 종료하는 경우입니다.
Exit Code: 1 (대부분)
로그 예시:
# Java
Exception in thread "main" java.lang.NullPointerException
at com.myapp.Application.main(Application.java:25)
# Python
Traceback (most recent call last):
File "/app/main.py", line 10, in <module>
from mymodule import missing_function
ImportError: cannot import name 'missing_function'
# Node.js
TypeError: Cannot read properties of undefined (reading 'port')
at Object.<anonymous> (/app/server.js:12:25)
실무 시나리오:
CI/CD에서 새 버전을 배포했는데, 코드에서 사용하는 라이브러리 버전이 바뀌면서 API 호환성이 깨졌습니다. 로컬과 CI 테스트에서는 잘 되지만 클러스터에서는 다른 환경변수 조합으로 실행되어 특정 코드 경로에서 예외가 발생합니다.
확인 방법:
# 이전 컨테이너 로그에서 에러 확인
kubectl logs <pod-name> --previous -n <namespace>
# 에러 발생 시점 확인 (시작 후 즉시 종료인지, 몇 초 후인지)
kubectl describe pod <pod-name> -n <namespace> | grep -A 5 "Last State"
해결:
- 로그의 에러 메시지를 기반으로 코드 수정
- 이전 버전으로 롤백 후 원인 분석
# 이전 버전으로 롤백
kubectl rollout undo deployment/<deployment-name> -n <namespace>
# 롤백 상태 확인
kubectl rollout status deployment/<deployment-name> -n <namespace>
3-2. 환경변수 / 설정 누락
애플리케이션이 시작 시 필요한 환경변수나 설정 파일이 없어서 종료하는 경우입니다. ConfigMap이나 Secret이 존재하지 않거나, 키 이름이 다른 경우에 발생합니다.
Exit Code: 1
로그 예시:
Error: DATABASE_URL environment variable is not set
Error: Failed to load config file: /etc/myapp/config.yaml: no such file or directory
panic: required key DB_PASSWORD missing value
실무 시나리오:
개발 환경에서는 .env 파일로 환경변수를 관리합니다. 스테이징 클러스터에 배포할 때 ConfigMap에 DATABASE_HOST는 넣었지만 DATABASE_PORT를 빠뜨렸습니다. 애플리케이션은 시작 시 모든 필수 환경변수를 체크하고, 하나라도 없으면 Exit 1로 종료합니다.
확인 방법:
# Pod에 주입된 환경변수 확인
kubectl exec <pod-name> -n <namespace> -- env
# CrashLoopBackOff 상태에서는 exec이 안 될 수 있음
# 대안: Pod spec에서 env 설정 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].env}' | jq .
# ConfigMap 존재 여부 확인
kubectl get configmap <configmap-name> -n <namespace>
# Secret 존재 여부 확인
kubectl get secret <secret-name> -n <namespace>
# ConfigMap/Secret의 키 목록 확인
kubectl get configmap <configmap-name> -n <namespace> -o jsonpath='{.data}' | jq 'keys'
해결:
# ConfigMap에 누락된 키 추가
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
namespace: production
data:
DATABASE_HOST: "db.internal.svc.cluster.local"
DATABASE_PORT: "5432" # 누락된 키 추가
DATABASE_NAME: "myapp"
# ConfigMap 업데이트 후 Pod 재시작
kubectl apply -f configmap.yaml
kubectl rollout restart deployment/<deployment-name> -n <namespace>
ConfigMap이나 Secret을 업데이트해도 기존 Pod에는 자동 반영되지 않습니다(envFrom 사용 시).
kubectl rollout restart로 Pod를 재생성해야 새 값이 적용됩니다. Volume으로 마운트한 경우에는 kubelet이 주기적으로 갱신하지만, 갱신 간격(기본 1분)이 있으므로 즉시 반영이 필요하면 재시작이 확실합니다.3-3. OOMKilled (메모리 limit 초과)
컨테이너의 메모리 사용량이 resources.limits.memory를 초과하면 Linux 커널의 OOM Killer가 프로세스를 강제 종료합니다.
Exit Code: 137 (128 + 9 = SIGKILL)
Events 메시지:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
실무 시나리오:
Java 애플리케이션의 메모리 limit을 512Mi로 설정했습니다. JVM의 -Xmx는 설정하지 않았습니다. JVM은 컨테이너의 메모리 제한을 인식하고 기본 힙을 제한의 25%(128Mi)로 잡지만, Metaspace, Thread Stack, Native Memory를 합치면 512Mi를 초과하여 OOMKilled가 발생합니다. 시작 직후에는 괜찮다가, 트래픽이 들어오면서 힙 사용량이 증가하여 종료됩니다.
확인 방법:
# 1. OOMKilled 여부 확인
kubectl describe pod <pod-name> -n <namespace> | grep -A 3 "Last State"
# 2. 현재 메모리 사용량 확인 (metrics-server 필요)
kubectl top pod <pod-name> -n <namespace>
# 3. Node 수준에서 OOM 이벤트 확인
kubectl describe node <node-name> | grep -i oom
# 4. 설정된 limit 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].resources}'
해결:
spec:
containers:
- name: my-app
resources:
requests:
memory: "512Mi" # 평상시 사용량 기준
limits:
memory: "1Gi" # 피크 사용량 + 여유분
메모리 설정 가이드라인:
| 항목 | 권장 방법 |
|---|---|
| requests | 평상시 메모리 사용량 (P50) |
| limits | 피크 사용량의 1.5~2배 또는 P99 |
| JVM 앱 | -Xmx를 limit의 70~75%로 설정 (나머지는 Metaspace, Stack용) |
| Node.js | --max-old-space-size를 limit의 75%로 설정 |
| 측정 | 부하 테스트 후 kubectl top pod로 실제 사용량 확인 |
OOMKilled가 반복되면 무조건 limit을 올리기보다, 먼저 메모리 프로파일링으로 누수 여부를 확인하는 것이 좋습니다. 누수가 있으면 limit을 아무리 올려도 시간이 지나면 다시 OOMKilled가 발생합니다.
3-4. Command / Entrypoint 오류
Pod spec의 command나 args가 잘못되었거나, Dockerfile의 ENTRYPOINT/CMD가 올바르지 않은 경우입니다.
Exit Code: 126 (실행 권한 없음) 또는 127 (명령어 없음)
로그 예시:
# Exit 127
exec /app/start.sh: no such file or directory
# Exit 126
exec /app/start.sh: permission denied
# Shell이 없는 scratch 이미지에서 shell 명령 실행 시도
exec: "/bin/sh": stat /bin/sh: no such file or directory
실무 시나리오:
Dockerfile에서 multi-stage build를 사용하여 최종 이미지를 scratch 또는 distroless 기반으로 만들었습니다. Pod spec에서 command: ["/bin/sh", "-c", "..."]를 사용했는데, 해당 이미지에는 shell이 없어서 127로 종료됩니다.
확인 방법:
# Pod spec의 command/args 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].command}'
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].args}'
# 이미지의 ENTRYPOINT/CMD 확인 (로컬에서)
docker inspect <image>:<tag> --format='{{.Config.Entrypoint}} {{.Config.Cmd}}'
해결:
# 1. command가 이미지 내 실제 바이너리 경로를 가리키는지 확인
spec:
containers:
- name: my-app
image: my-app:v1.2.3
command: ["/app/my-app"] # 바이너리 직접 실행
# command: ["/bin/sh", "-c"] # scratch 이미지에서는 사용 불가
# 2. 실행 권한 문제라면 Dockerfile에서 수정
# Dockerfile
RUN chmod +x /app/start.sh
3-5. Liveness Probe 실패
Liveness Probe가 실패하면 kubelet이 컨테이너를 강제 재시작합니다. 앱 시작 시간보다 initialDelaySeconds가 짧으면, 앱이 준비되기 전에 Probe가 실패하여 무한 재시작에 빠집니다.
Events 메시지:
Warning Unhealthy 5s (x3 over 15s) kubelet Liveness probe failed:
Get "http://10.0.1.5:8080/health": dial tcp 10.0.1.5:8080: connect: connection refused
Warning Unhealthy 5s kubelet Liveness probe failed: HTTP probe failed with statuscode: 503
실무 시나리오:
Spring Boot 애플리케이션의 시작 시간이 30초입니다. Liveness Probe의 initialDelaySeconds를 10초로 설정했습니다. 앱이 아직 시작 중인데 Probe가 먼저 실행되어 실패합니다. 3회 연속 실패(failureThreshold 기본값) 후 컨테이너가 재시작되고, 다시 30초 걸려서 시작하는데 또 10초 만에 Probe가 실행됩니다. 무한 루프입니다.
확인 방법:
# 1. Probe 설정 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].livenessProbe}' | jq .
# 2. 앱 실제 시작 시간 확인 (로그에서)
kubectl logs <pod-name> --previous -n <namespace> | grep -i "started\|ready\|listening"
# 3. Events에서 Probe 실패 확인
kubectl describe pod <pod-name> -n <namespace> | grep -i "unhealthy\|probe"
해결 — Startup Probe 사용 (권장):
Kubernetes 1.20+ 에서는 startupProbe를 사용하여 시작 시간이 긴 앱을 안전하게 처리할 수 있습니다. startupProbe가 성공할 때까지 livenessProbe와 readinessProbe는 비활성화됩니다.
spec:
containers:
- name: my-app
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 5
failureThreshold: 30 # 5초 × 30회 = 최대 150초 대기
해결 — initialDelaySeconds 조정 (startupProbe 미사용 시):
spec:
containers:
- name: my-app
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 60 # 앱 시작 시간보다 충분히 길게
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
Probe 설정 설계 기준:
| 파라미터 | 권장값 산정 방법 |
|---|---|
initialDelaySeconds |
앱 시작 시간 P95 + 여유 10초 (startupProbe 없을 때) |
periodSeconds |
10~30초 (너무 짧으면 불필요한 부하) |
failureThreshold |
3 (기본값, 일시적 실패 허용) |
timeoutSeconds |
앱 health endpoint 응답 P99 + 1~2초 |
Liveness Probe 실패 → 컨테이너 재시작 (강제 종료). Readiness Probe 실패 → Service에서 트래픽만 제외 (컨테이너 유지). 시작 시간이 긴 앱에서 Liveness Probe를 너무 공격적으로 설정하면 무한 재시작 루프에 빠집니다. 시작 완료 전까지는 startupProbe로 보호하고, 이후에만 livenessProbe를 동작시키는 것이 안전합니다.
3-6. 의존 서비스 연결 실패
애플리케이션이 시작 시 데이터베이스, Redis, 외부 API 등에 연결을 시도하는데, 해당 서비스가 아직 준비되지 않았거나 접근이 차단된 경우입니다.
Exit Code: 1
로그 예시:
Error: connect ECONNREFUSED 10.100.50.30:5432
Failed to connect to Redis at redis.default.svc.cluster.local:6379: Connection refused
panic: dial tcp 10.100.50.30:3306: i/o timeout
실무 시나리오:
마이크로서비스 환경에서 여러 Deployment를 동시에 배포합니다. App 서비스가 DB 서비스보다 먼저 시작되면 연결에 실패합니다. 또는 NetworkPolicy가 적용된 클러스터에서 새 서비스를 추가했는데, 해당 서비스에 대한 Ingress 규칙을 빠뜨린 경우에도 동일한 증상이 나타납니다.
확인 방법:
# 1. 의존 서비스 상태 확인
kubectl get pods -n <namespace> | grep <dependency-name>
kubectl get svc -n <namespace> | grep <dependency-name>
# 2. Pod에서 의존 서비스로 연결 테스트 (디버그 컨테이너 사용)
kubectl debug -it <pod-name> --image=busybox -- sh
# 또는 임시 Pod로 테스트
kubectl run test-net --rm -it --image=busybox -- sh
nslookup <service-name>.<namespace>.svc.cluster.local
nc -zv <service-name>.<namespace>.svc.cluster.local <port>
# 3. NetworkPolicy 확인
kubectl get networkpolicy -n <namespace>
kubectl describe networkpolicy <policy-name> -n <namespace>
해결:
- initContainer로 의존 서비스 대기:
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db.default.svc.cluster.local 5432; do echo waiting for db; sleep 2; done']
containers:
- name: my-app
image: my-app:v1.2.3
- 앱 레벨에서 재시도 로직 구현:
연결 실패 시 즉시 종료하지 않고, 지수 백오프로 재시도하는 것이 운영 환경에서는 더 안정적입니다. initContainer 방식은 시작 시에만 체크하므로, 시작 후 의존 서비스가 잠시 다운되면 대응할 수 없습니다.
- NetworkPolicy 규칙 추가:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: default
spec:
podSelector:
matchLabels:
app: my-db
ingress:
- from:
- podSelector:
matchLabels:
app: my-app
ports:
- port: 5432
protocol: TCP
3-7. Volume 마운트 권한 문제
컨테이너가 Volume에 쓰기를 시도하는데 권한이 없어서 실패하는 경우입니다. 특히 non-root 사용자로 실행하는 컨테이너에서 자주 발생합니다.
Exit Code: 1
로그 예시:
Error: EACCES: permission denied, open '/data/logs/app.log'
mkdir: cannot create directory '/data/cache': Permission denied
실무 시나리오:
보안을 위해 Dockerfile에서 USER 1000으로 non-root 실행을 설정했습니다. PersistentVolume을 /data에 마운트했는데, 해당 볼륨의 소유자가 root여서 UID 1000인 프로세스가 쓰기를 할 수 없습니다.
확인 방법:
# 1. 컨테이너 실행 사용자 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].securityContext}'
# 2. Pod 수준 securityContext 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.securityContext}'
해결:
spec:
securityContext:
fsGroup: 1000 # 마운트된 볼륨의 그룹을 1000으로 변경
runAsUser: 1000
runAsGroup: 1000
containers:
- name: my-app
image: my-app:v1.2.3
volumeMounts:
- name: data
mountPath: /data
fsGroup을 설정하면 마운트된 볼륨의 모든 파일에 해당 GID가 소유 그룹으로 적용됩니다. 다만 대용량 볼륨에서는 마운트 시 모든 파일의 권한을 변경하느라 시작이 느려질 수 있습니다. Kubernetes 1.23+에서는 fsGroupChangePolicy: OnRootMismatch를 사용하면 루트 디렉토리 권한이 다를 때만 변경하여 시작 시간을 단축할 수 있습니다.3-8. 프로세스가 포그라운드에서 실행되지 않음
컨테이너의 메인 프로세스(PID 1)가 백그라운드로 전환되면 즉시 종료된 것으로 인식됩니다. Kubernetes는 PID 1 프로세스가 살아있는 동안만 컨테이너가 실행 중이라고 판단합니다.
Exit Code: 0
실무 시나리오:
Nginx를 컨테이너로 실행하는데, 설정에 daemon off; 를 넣지 않았습니다. Nginx가 시작되면 마스터 프로세스가 백그라운드로 전환(daemonize)하면서 포그라운드 프로세스가 종료됩니다. Exit 0이지만 restartPolicy: Always에 의해 계속 재시작됩니다.
해결:
# Nginx 예시 — 포그라운드 실행
CMD ["nginx", "-g", "daemon off;"]
# 일반적인 셸 스크립트 — exec으로 PID 1 전달
#!/bin/sh
exec java -jar /app/my-app.jar
# exec 없이 실행하면 sh가 PID 1이 되고, java는 자식 프로세스
| 소프트웨어 | 포그라운드 실행 방법 |
|---|---|
| Nginx | nginx -g "daemon off;" |
| Apache | httpd -DFOREGROUND |
| Redis | 기본이 포그라운드 (설정에서 daemonize no) |
| MySQL | mysqld (기본 포그라운드) |
| 커스텀 스크립트 | exec 으로 메인 프로세스를 PID 1로 실행 |
4. 원인 구조 한눈에 보기
5. 실무 시나리오: 배포 직후 CrashLoopBackOff 디버깅
상황: CI/CD로 새 버전(v2.1.0)을 배포했는데, 새 Pod가 CrashLoopBackOff에 빠집니다. 이전 버전(v2.0.0) Pod는 정상 동작 중입니다.
진단 과정:
# 1. 상태 확인
kubectl get pods -n production | grep my-app
# my-app-v2-abc123 0/1 CrashLoopBackOff 5 3m
# my-app-v1-def456 1/1 Running 0 2d
# 2. Exit Code 확인
kubectl describe pod my-app-v2-abc123 -n production | grep -A 5 "Last State"
# Last State: Terminated
# Reason: Error
# Exit Code: 1
# 3. 이전 로그 확인
kubectl logs my-app-v2-abc123 --previous -n production
# 2026-06-05T10:30:01Z ERROR Failed to initialize cache connection
# 2026-06-05T10:30:01Z ERROR Required environment variable REDIS_URL is not set
# 2026-06-05T10:30:01Z FATAL Application startup failed, exiting
# 4. 원인 식별: 환경변수 누락
# v2.1.0에서 새로 추가된 Redis 캐시 기능에 REDIS_URL이 필요한데
# ConfigMap에 아직 추가되지 않았음
# 5. 해결
kubectl edit configmap my-app-config -n production
# REDIS_URL: "redis://redis.production.svc.cluster.local:6379" 추가
kubectl rollout restart deployment/my-app -n production
kubectl rollout status deployment/my-app -n production
# deployment "my-app" successfully rolled out
사후 분석:
- CI/CD 파이프라인에서 새 환경변수가 추가되면 ConfigMap 업데이트를 포함시키는 프로세스가 필요합니다.
- 앱에서 필수 환경변수 체크 시 명확한 에러 메시지를 출력하도록 구현하면 디버깅 시간이 단축됩니다.
- Helm values 또는 Kustomize overlay에서 환경별 설정을 관리하면 누락 위험을 줄일 수 있습니다.
6. 디버깅 도구와 기법
6-1. Ephemeral Debug Container
CrashLoopBackOff 상태에서는 kubectl exec이 동작하지 않습니다(컨테이너가 실행 중이지 않으므로). Kubernetes 1.25+에서는 Ephemeral Container를 사용하여 실행 중이지 않은 Pod에도 디버그 컨테이너를 붙일 수 있습니다.
# 디버그 컨테이너 추가
kubectl debug -it <pod-name> --image=busybox --target=<container-name> -n <namespace>
# 또는 Node 수준에서 디버깅
kubectl debug node/<node-name> -it --image=ubuntu
6-2. Command 오버라이드로 진입
컨테이너가 시작 즉시 종료되어 로그도 확인이 어려운 경우, command를 sleep으로 오버라이드하여 컨테이너를 살려둔 상태에서 내부를 조사할 수 있습니다.
# Deployment의 command를 일시적으로 오버라이드
kubectl patch deployment <name> -n <namespace> --type='json' \
-p='[{"op": "replace", "path": "/spec/template/spec/containers/0/command", "value": ["sleep", "3600"]}]'
# 컨테이너 접속 후 수동으로 앱 실행하여 에러 확인
kubectl exec -it <pod-name> -n <namespace> -- sh
/app/start.sh # 직접 실행하여 에러 확인
이 방법은 디버깅 전용입니다. 조사가 끝나면 반드시 원래 command로 복원하세요. 프로덕션에서는 별도 디버깅용 Pod를 생성하는 것이 안전합니다.
6-3. 유용한 디버깅 명령어 모음
# Pod 전체 상태 YAML로 확인
kubectl get pod <pod-name> -n <namespace> -o yaml
# 재시작 횟수와 마지막 종료 시점 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[0].restartCount}'
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[0].lastState}'
# 특정 네임스페이스에서 CrashLoopBackOff인 Pod만 필터링
kubectl get pods -n <namespace> --field-selector=status.phase!=Running | grep CrashLoopBackOff
# 클러스터 전체에서 CrashLoopBackOff Pod 찾기
kubectl get pods --all-namespaces | grep CrashLoopBackOff
# Events를 시간순으로 정렬하여 확인
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep <pod-name>
7. 재발 방지 체크리스트
| 항목 | 권장 설정 | 이유 |
|---|---|---|
| Startup Probe | 시작 시간이 긴 앱에 설정 | Liveness Probe에 의한 무한 재시작 방지 |
| 메모리 requests/limits | 부하 테스트 후 실측값 기반 설정 | OOMKilled 방지, 추측 기반 설정 위험 |
| 필수 환경변수 검증 | 앱 시작 시 누락 체크 + 명확한 에러 메시지 | 디버깅 시간 단축 |
| Readiness Probe | 의존 서비스 포함 체크 | 준비되지 않은 Pod로 트래픽 유입 방지 |
| initContainer | 필수 의존 서비스 대기 | 의존 서비스 미준비로 인한 반복 실패 방지 |
| CI/CD 환경 검증 | 배포 전 ConfigMap/Secret 존재 확인 | 설정 누락으로 인한 배포 실패 방지 |
| 이미지 빌드 | 포그라운드 실행, 적절한 ENTRYPOINT | Exit 0 무한 재시작 방지 |
| 롤백 전략 | maxUnavailable, maxSurge 설정 |
실패 시 빠른 자동 롤백 |
8. 정리
- CrashLoopBackOff는 "컨테이너가 반복 종료 후 재시작 대기 중"이라는 상태입니다. 원인은 Exit Code와 로그에서 확인합니다.
- Exit Code 1(일반 에러)이 가장 흔하고, 로그의 에러 메시지가 직접적인 원인을 알려줍니다. Exit Code 137은 OOMKilled를 의미합니다.
- Liveness Probe 설정 오류로 인한 무한 재시작은 자주 발생하는 실수입니다.
startupProbe를 사용하면 시작 시간이 긴 앱도 안전하게 보호할 수 있습니다. - 새 버전 배포 후 발생하면 로그 확인 → 롤백 → 원인 분석 순서로 대응합니다. 운영 환경에서는 서비스 복구가 디버깅보다 우선입니다.
- 의존 서비스 연결 실패는 initContainer나 앱 레벨 재시도 로직으로 방어합니다. 둘 다 적용하면 더 안정적입니다.
참고 문서
'Troubleshooting' 카테고리의 다른 글
| EKS Pod가 외부 인터넷에 접근하지 못하는 경우 (0) | 2026.06.06 |
|---|---|
| Terraform Error acquiring the state lock 해결 방법 (0) | 2026.06.05 |
| S3 AccessDenied 원인과 해결 방법: Bucket Policy, IAM, KMS, VPC Endpoint까지 (0) | 2026.06.01 |
| Kubernetes ImagePullBackOff 원인과 해결 방법 (0) | 2026.06.01 |
| ALB 502 Bad Gateway 원인 분석: Target Group, Health Check, 타임아웃까지 (0) | 2026.05.31 |