들어가며
Kubernetes 클러스터를 운영하다 보면 가장 자주 마주치는 에러 중 하나가 바로 CrashLoopBackOff입니다. Pod가 시작되자마자 죽고, 다시 시작되고, 또 죽는 무한 루프에 빠진 상태죠. 특히 새벽에 알림이 울릴 때 이 상태를 보면 심장이 철렁합니다.
문제는 CrashLoopBackOff 자체가 원인이 아니라 증상이라는 점입니다. 실제 원인은 OOMKilled, 설정 오류, 의존성 실패 등 다양합니다. 오늘은 실무에서 이 상태를 체계적으로 진단하고 해결하는 방법을 정리합니다.
CrashLoopBackOff란?
Kubernetes는 컨테이너가 비정상 종료되면 restartPolicy에 따라 자동으로 재시작합니다. 그런데 컨테이너가 반복적으로 실패하면 kubelet은 재시작 간격을 점점 늘립니다(10초 → 20초 → 40초 → ... 최대 5분). 이 백오프 상태가 바로 CrashLoopBackOff입니다.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-7d4b8c6f5-x2k9m 0/1 CrashLoopBackOff 7 (2m ago) 12m
1단계: 기본 진단 — describe와 logs
가장 먼저 확인할 것은 Pod의 이벤트와 로그입니다.
# Pod 상태와 이벤트 확인
kubectl describe pod my-app-7d4b8c6f5-x2k9m -n default
# 현재(또는 마지막) 컨테이너 로그
kubectl logs my-app-7d4b8c6f5-x2k9m -n default
# 이전에 죽은 컨테이너 로그 (핵심!)
kubectl logs my-app-7d4b8c6f5-x2k9m -n default --previous
--previous 플래그는 필수입니다. 현재 컨테이너는 아직 시작 중이거나 이미 새로 떴기 때문에, 실제 에러 메시지는 이전 컨테이너 로그에 남아 있습니다.
2단계: 흔한 원인별 해결법
OOMKilled — 메모리 부족
$ kubectl describe pod my-app-xxx | grep -A 5 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Exit Code 137은 SIGKILL(메모리 초과)을 의미합니다. 해결 방법:
# deployment.yaml
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi" # 실제 사용량 기반으로 조정
실제 메모리 사용량을 먼저 확인하세요:
# metrics-server 설치 필요
kubectl top pod my-app-xxx -n default
# 또는 Prometheus 쿼리
# container_memory_working_set_bytes{pod="my-app-xxx"}
설정/시크릿 누락
$ kubectl logs my-app-xxx --previous
Error: config file /etc/app/config.yaml not found
ConfigMap이나 Secret이 마운트되지 않았거나, 키 이름이 틀린 경우입니다:
# ConfigMap 존재 확인
kubectl get configmap my-app-config -n default -o yaml
# Secret 존재 확인
kubectl get secret my-app-secret -n default
의존 서비스 연결 실패
데이터베이스, Redis 등 외부 서비스에 연결하지 못해 앱이 시작 즉시 종료되는 경우입니다:
# initContainer로 의존성 대기 처리
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c',
'until nc -z postgres-svc 5432; do echo "DB 대기중..."; sleep 2; done']
잘못된 컨테이너 명령어
Dockerfile의 ENTRYPOINT나 Kubernetes의 command/args 설정 오류:
$ kubectl logs my-app-xxx --previous
exec /app/start.sh: no such file or directory
# 잘못된 예
command: ["/app/start.sh"] # 파일이 없거나 실행 권한 없음
# 디버깅: 임시로 sleep으로 교체 후 쉘 접속
command: ["sleep", "3600"]
# sleep 상태에서 컨테이너 진입하여 파일 확인
kubectl exec -it my-app-xxx -- /bin/sh
ls -la /app/
3단계: 고급 진단 도구
이벤트 시간순 정렬로 전체 흐름 파악
kubectl get events -n default --sort-by='.lastTimestamp' | grep my-app
ephemeral container로 라이브 디버깅 (k8s 1.25+)
kubectl debug -it my-app-xxx --image=busybox:1.36 --target=my-app
crictl로 노드 레벨 컨테이너 상태 확인
# 노드에 SSH 접속 후
crictl ps -a | grep my-app
crictl logs <container-id>
주의사항 및 베스트 프랙티스
리소스 limits는 반드시 설정하세요. limits 없이 운영하면 노드 전체에 영향을 줄 수 있습니다. 다만 CPU limits는 throttling을 유발할 수 있으므로 requests만 설정하고 limits는 생략하는 전략도 고려하세요.
Liveness Probe를 너무 공격적으로 설정하지 마세요. initialDelaySeconds가 너무 짧으면 앱이 아직 초기화 중인데 probe가 실패하여 CrashLoopBackOff처럼 보이는 재시작 루프에 빠집니다. Startup Probe를 활용하면 초기화 시간이 긴 앱도 안전하게 처리할 수 있습니다:
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10 # 최대 300초 대기
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
kubectl logs --previous를 습관화하세요. 현재 로그가 비어 있어도 이전 컨테이너 로그에 원인이 있습니다.
마무리
CrashLoopBackOff를 만났을 때의 체크리스트를 정리하면:
kubectl describe pod→ 이벤트와 Exit Code 확인kubectl logs --previous→ 실제 에러 메시지 확인- Exit Code로 원인 분류 (137=OOM, 1=앱에러, 126/127=명령어 오류)
- 리소스, ConfigMap, Secret, 의존 서비스 순서로 점검
- 해결이 안 되면
sleep명령어로 컨테이너를 살려둔 뒤 직접 진입하여 디버깅
추가 참고 자료:
'Kubernets' 카테고리의 다른 글
| Cluster Autoscaler에서 Karpenter로 옮기다 새벽에 멘탈 나간 썰 (0) | 2026.04.25 |
|---|---|
| kubectl debug로 Kubernetes Pod 트러블슈팅 완전 정복 (0) | 2026.04.25 |