Kubernetes CrashLoopBackOff: 파드가 계속 재시작되는 원인과 해결법 완벽 가이드
“저 파드 왜 자꾸 Restart 횟수가 올라가요?”
월요일 아침, 출근하자마자 슬랙 알람이 쏟아집니다. kubectl get pods를 치니 눈앞이 빨개집니다.
NAME READY STATUS RESTARTS AGE
my-app-7d4b5c8f9-xk2mv 0/1 CrashLoopBackOff 15 12m
CrashLoopBackOff. 쿠버네티스를 운영하다 보면 OOMKilled만큼이나 자주 마주치는 골치 아픈 상태입니다.
파드가 시작되고, 곧바로 죽고, 쿠버네티스가 다시 살리고, 또 죽고… 이 무한 루프가 바로 CrashLoopBackOff입니다. 더 짜증나는 건, 쿠버네티스가 “야, 너 자꾸 죽어서 이제 천천히 다시 시작시킬게”라며 재시작 간격을 점점 늘린다는 것입니다.
10초 → 20초 → 40초 → 80초 → … → 최대 5분
디버깅하려고 로그를 보려는데, 파드가 5분마다 떴다 죽었다 하니 답답해서 미칠 지경이죠.
CrashLoopBackOff의 정체: 쿠버네티스의 자기 보호 본능
CrashLoopBackOff는 에러 그 자체가 아니라, “에러가 계속 발생하고 있다”는 상태 표시입니다.
쿠버네티스 입장에서 생각해 봅시다. 컨테이너가 시작되자마자 죽으면 당연히 재시작시킵니다. 그런데 재시작해도 또 죽으면? 또 재시작합니다. 이게 10번, 20번 반복되면 어떻게 될까요?
시스템 리소스가 낭비되고, CPU가 무의미하게 소모됩니다. 그래서 쿠버네티스는 지수 백오프(Exponential Backoff) 전략을 사용해서 재시작 간격을 점점 늘립니다. 이게 바로 ‘BackOff’의 의미입니다.
핵심 포인트: 진짜 에러는 다른 곳에 있다
CrashLoopBackOff를 해결하려면 **“왜 컨테이너가 죽는가?”**를 찾아야 합니다. CrashLoopBackOff 자체를 고치는 게 아니라, 그 원인이 되는 진짜 문제를 해결해야 합니다.
Exit Code로 범인 추적하기
컨테이너가 죽을 때 남기는 Exit Code는 결정적인 단서입니다.
| Exit Code | 의미 | 일반적인 원인 |
|---|---|---|
| 0 | 정상 종료 | 프로세스가 의도대로 종료됨 (Job에서는 정상, Deployment에서는 문제) |
| 1 | 일반 에러 | 애플리케이션 레벨 에러, 코드 버그, 설정 오류 |
| 126 | 실행 불가 | 명령어를 찾았지만 실행 권한이 없음 |
| 127 | 명령어 없음 | 지정된 명령어나 바이너리를 찾을 수 없음 |
| 137 | SIGKILL (9) | OOMKilled, 강제 종료 (128 + 9) |
| 139 | SIGSEGV (11) | Segmentation Fault, 잘못된 메모리 접근 |
| 143 | SIGTERM (15) | 정상적인 종료 요청 (128 + 15) |
Exit Code 확인하는 법
kubectl describe pod <pod-name> | grep -A 5 "Last State"
출력 예시:
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Mon, 20 Jan 2026 09:15:23 +0900
Finished: Mon, 20 Jan 2026 09:15:25 +0900
Exit Code가 1이면 애플리케이션 에러, 137이면 메모리 문제(OOMKilled)를 의심하세요.
7가지 흔한 원인과 해결책
1. 애플리케이션 코드 에러 (Exit Code 1)
증상: 컨테이너가 시작되자마자 즉시 종료됩니다. 로그에 Python traceback이나 Java stack trace가 출력됩니다.
원인: 코드 버그, 초기화 실패, 의존성 누락 등
디버깅:
# 이전 컨테이너의 로그 확인 (중요!)
kubectl logs <pod-name> --previous
# 더 자세한 정보
kubectl describe pod <pod-name>
해결책: 로그를 분석해서 코드를 수정합니다. 로컬에서 동일한 이미지로 테스트해 보세요.
# 로컬에서 동일한 이미지 테스트
docker run -it <image-name> /bin/sh
2. 잘못된 명령어/엔트리포인트 (Exit Code 126, 127)
증상: command not found, permission denied 에러가 로그에 출력됩니다.
원인: Dockerfile의 ENTRYPOINT나 Kubernetes의 command 설정 오류
디버깅:
# 컨테이너 내부에서 직접 확인
kubectl run debug --image=<your-image> --rm -it -- /bin/sh
# 명령어 경로 확인
which node
which python
해결책:
# 잘못된 예
spec:
containers:
- name: app
command: ["node"] # 경로가 없음
args: ["app.js"]
# 올바른 예
spec:
containers:
- name: app
command: ["/usr/local/bin/node"] # 전체 경로 명시
args: ["/app/app.js"]
3. 설정 파일/환경 변수 누락 (Exit Code 1)
증상: Config file not found, Environment variable X is required 같은 에러
원인: ConfigMap, Secret이 마운트되지 않았거나, 필수 환경변수가 설정되지 않음
디버깅:
# 환경변수 확인
kubectl exec <pod-name> -- env | grep DATABASE
# ConfigMap 상태 확인
kubectl get configmap <configmap-name> -o yaml
# Secret 상태 확인 (base64 디코딩 필요)
kubectl get secret <secret-name> -o jsonpath='{.data.password}' | base64 -d
해결책:
spec:
containers:
- name: app
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
envFrom:
- configMapRef:
name: app-config
4. Liveness Probe 실패
증상: 컨테이너는 시작되지만, 일정 시간 후 계속 재시작됩니다. Events에 Liveness probe failed 메시지가 있습니다.
원인: Liveness Probe가 너무 공격적으로 설정되었거나, 애플리케이션 시작 시간이 오래 걸림
디버깅:
kubectl describe pod <pod-name> | grep -A 10 "Liveness"
해결책:
# 너무 공격적인 설정 (문제)
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5 # 시작 5초 후 바로 체크
periodSeconds: 5 # 5초마다 체크
failureThreshold: 1 # 1번 실패하면 바로 재시작
# 적절한 설정 (해결)
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30 # 충분한 시작 시간 확보
periodSeconds: 10 # 10초마다 체크
failureThreshold: 3 # 3번 연속 실패해야 재시작
# 시작 시간이 오래 걸리는 앱이라면 startupProbe 사용
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 10 # 최대 5분까지 기다림
:::tip Startup Probe vs Liveness Probe
startupProbe: 앱이 완전히 시작될 때까지만 사용. 성공하면 비활성화됨livenessProbe: 앱 실행 중 건강 상태 체크. 실패하면 재시작- Java나 .NET 같이 시작 시간이 오래 걸리는 앱에는
startupProbe를 꼭 설정하세요! :::
5. OOMKilled (Exit Code 137)
증상: Exit Code가 137이고, kubectl describe pod에서 Reason: OOMKilled가 보입니다.
원인: 컨테이너가 설정된 메모리 limit을 초과함
디버깅:
kubectl describe pod <pod-name> | grep -A 3 "Last State"
해결책:
resources:
requests:
memory: "256Mi"
limits:
memory: "512Mi" # request의 1.5~2배 정도로 설정
:::note OOMKilled에 대한 더 자세한 내용은 Kubernetes OOMKilled: 내 파드가 자꾸 죽는 진짜 이유 (Exit Code 137) 포스트를 참고하세요. :::
6. 이미지 Pull 실패 후 CrashLoopBackOff
증상: 처음엔 ImagePullBackOff였다가 나중에 CrashLoopBackOff로 바뀜
원인: 이미지가 손상되었거나, 플랫폼(amd64/arm64) 불일치
디버깅:
kubectl describe pod <pod-name> | grep -A 5 "Events"
# 노드의 아키텍처 확인
kubectl get nodes -o wide
해결책:
# 멀티 아키텍처 이미지 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
7. 볼륨 마운트 실패
증상: Unable to mount volumes, subPath 관련 에러
원인: PVC가 아직 Bound되지 않았거나, subPath가 존재하지 않음
디버깅:
# PVC 상태 확인
kubectl get pvc
# PV 상태 확인
kubectl get pv
해결책:
# PVC가 Bound될 때까지 기다리는 initContainer 사용
initContainers:
- name: wait-for-volume
image: busybox
command: ['sh', '-c', 'until [ -d /data ]; do sleep 1; done']
volumeMounts:
- name: data-volume
mountPath: /data
실전 디버깅 플로우차트
CrashLoopBackOff를 만나면 이 순서로 디버깅하세요:
1. kubectl describe pod <pod-name>
→ Events 섹션에서 에러 메시지 확인
→ Last State에서 Exit Code 확인
2. kubectl logs <pod-name> --previous
→ 이전 컨테이너의 로그 확인 (매우 중요!)
3. Exit Code에 따른 조치:
- 1: 애플리케이션 로그 분석, 코드/설정 확인
- 126/127: 명령어 경로, 실행 권한 확인
- 137: 메모리 limit 확인, OOMKilled 대응
- 139: 코드의 메모리 접근 버그 확인
4. 그래도 안 되면 디버그 컨테이너 사용:
kubectl debug <pod-name> -it --image=busybox -- /bin/sh
디버깅 꿀팁: --previous 플래그
CrashLoopBackOff 상황에서 가장 많이 하는 실수가 kubectl logs <pod-name>만 치는 것입니다.
문제는 이 명령어가 현재 실행 중인 컨테이너의 로그를 보여준다는 점입니다.
CrashLoopBackOff 상태에서는 컨테이너가 시작되자마자 죽기 때문에, 로그를 보는 시점에 이미 새 컨테이너가 떴을 수 있습니다.
반드시 --previous 플래그를 사용하세요!
# 이전(죽은) 컨테이너의 로그 확인
kubectl logs <pod-name> --previous
# 여러 컨테이너가 있는 경우
kubectl logs <pod-name> -c <container-name> --previous
예방이 최고의 치료
1. 로컬에서 먼저 테스트
# 프로덕션과 동일한 환경으로 로컬 테스트
docker run -it \
-e DATABASE_URL=xxx \
-e API_KEY=xxx \
<your-image>
2. 적절한 Probe 설정
# 모든 프로덕션 워크로드에 기본으로 설정
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
3. 리소스 제한 설정
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
4. 모니터링 및 알람 설정
Prometheus + Grafana로 파드 재시작 횟수를 모니터링하세요:
# 최근 1시간 동안 재시작 횟수가 5번 이상인 파드
sum(increase(kube_pod_container_status_restarts_total[1h])) by (pod, namespace) > 5
마치며
CrashLoopBackOff는 쿠버네티스가 “뭔가 잘못됐어!”라고 외치는 신호입니다. 당황하지 말고 체계적으로 접근하세요:
- Exit Code 확인 → 범인의 윤곽을 잡고
--previous로그 확인 → 결정적 증거를 찾고- 원인별 해결 → 근본 원인을 제거
특히 kubectl logs --previous는 정말 중요합니다. 이것만 기억해도 디버깅 시간을 절반으로 줄일 수 있습니다.
오늘도 여러분의 파드가 무사히 Running 상태로 남아있길 바랍니다!
관련 글: