CrashLoopBackOff 완벽 해결: kubectl로 Pod 재시작 무한루프 5분 진단
K8s Troubleshooting Guide 18편
"Back-off restarting failed container" — 지금 이 화면을 보고 있다면
배포는 끝났는데 Pod가 떴다 죽기를 반복하고, RESTARTS 카운트만 계속 올라가는 상황. 십중팔구 아래 화면을 보고 있을 겁니다.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-server-7d9f8c-x2kpq 0/1 CrashLoopBackOff 6 (90s ago) 8mSTATUS가 CrashLoopBackOff고 RESTARTS가 계속 증가하면, 쿠버네티스는 "이 컨테이너 또 죽었네, 잠깐 기다렸다 다시 띄울게"를 반복 중입니다. 재시작 간격은 10초 → 20초 → 40초… 최대 5분까지 지수적으로 늘어납니다(back-off).
헷갈리는 형제 에러부터 한 줄로 정리하고 갑시다.
- ImagePullBackOff: 컨테이너가 아예 안 뜸. 이미지를 못 가져옴(레지스트리/태그/인증 문제).
- OOMKilled: 메모리 한도 초과로 커널이 죽임(Exit 137). → CrashLoop의 한 원인이기도 함.
- CrashLoopBackOff: 컨테이너는 떴다가 죽기를 반복. ← 이 글이 정조준하는 케이스.
즉 "이미지는 잘 받았는데 프로세스가 시작 직후 또는 잠시 후 종료된다"가 핵심입니다.
1차 진단 3종 세트: describe · logs --previous · Exit Code
원인을 추측하지 말고 명령어로 좁힙니다. 아래 세 개만 순서대로 치세요.
① describe로 Events와 Last State 확인
kubectl describe pod api-server-7d9f8c-x2kpq출력에서 볼 핵심 구간:
Last State: Terminated
Reason: Error
Exit Code: 1
Started: Wed, 25 Jun 2026 10:11:02 +0900
Finished: Wed, 25 Jun 2026 10:11:03 +0900
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning BackOff 20s (x6 over 7m) kubelet Back-off restarting failed containerLast State: Terminated의 Exit Code와 Reason이 1차 단서입니다. Started와 Finished 시각 차이를 보면 "1초 만에 죽었는지", "한참 돌다 죽었는지"도 알 수 있습니다.
② 죽기 직전 로그 보기 (가장 중요)
현재 컨테이너는 죽고 새로 떠서 kubectl logs가 비어 있을 때가 많습니다. 직전(죽은) 컨테이너 로그를 봐야 합니다.
kubectl logs api-server-7d9f8c-x2kpq --previous--previous(또는 -p)가 CrashLoop 진단의 핵심입니다. 대부분의 진짜 원인(스택트레이스, "config not found", "connection refused")이 여기 찍혀 있습니다.
③ Exit Code를 정확히 뽑기
kubectl get pod api-server-7d9f8c-x2kpq \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
# 이벤트만 시간순으로
kubectl get events --field-selector involvedObject.name=api-server-7d9f8c-x2kpq \
--sort-by=.lastTimestampExit Code 진단표
| Exit Code | 의미 | 추정 원인 | 다음 확인 액션 |
|---|---|---|---|
| 0 | 정상 종료인데 재시작 | 메인 프로세스가 할 일 끝나고 종료(배치성), restartPolicy 부적합 | Job/CronJob으로 바꾸거나 foreground 프로세스 유지 확인 |
| 1 | 앱 일반 오류 | 코드 예외, 환경변수/Config 누락, DB 연결 실패 | logs --previous로 스택트레이스, env/ConfigMap 점검 |
| 2 | 셸/인자 오류 | 잘못된 옵션, 셸 스크립트 문법 오류 | entrypoint 스크립트, command/args 검토 |
| 126 | 실행 권한 없음 | 바이너리에 실행 비트 없음, 스크립트 chmod 누락 | 이미지 내 ls -l, Dockerfile RUN chmod +x |
| 127 | 명령어 없음 | command 오타, 바이너리 미설치, PATH 문제 | command/args 경로 확인, 베이스 이미지 점검 |
| 137 | SIGKILL (128+9) | OOMKilled 또는 강제 종료 | describe의 Reason: OOMKilled, 메모리 limit 점검 |
| 143 | SIGTERM (128+15) | 정상 종료 신호 받음(롤링/preStop) | graceful shutdown 처리, probe 타이밍 점검 |
원인별 복붙 처방전 5가지
1) 잘못된 command/args (Exit 127/126)
증상: logs --previous에 exec: "start.sh": not found 또는 permission denied.
확인:
kubectl get pod <pod> -o jsonpath='{.spec.containers[0].command}'수정 YAML (경로·권한 교정):
spec:
containers:
- name: api
image: myregistry/api:1.4.0
command: ["/app/bin/server"] # 절대경로, 실제 존재하는 바이너리
args: ["--port=8080"]스크립트라면 이미지 빌드 시 RUN chmod +x /app/bin/server를 잊지 마세요(126 방지).
2) 의존 서비스 미준비 (initContainer로 대기)
증상: 앱이 DB/Redis에 붙으려다 connection refused로 Exit 1, 의존 서비스가 늦게 뜨면 무한 재시작.
확인:
kubectl logs <pod> --previous | grep -i "refused\|timeout\|unreachable"수정 YAML (wait-for 패턴):
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z postgres 5432; do
echo "waiting for postgres..."; sleep 2;
done
containers:
- name: api
image: myregistry/api:1.4.03) 환경변수·ConfigMap/Secret 누락 (Exit 1)
GitOps 환경에서 ConfigMap만 빠진 채 Deployment가 먼저 동기화되면 단골로 터지는 케이스입니다.
증상: Error: configmap "app-config" not found 또는 앱 로그에 KeyError: DATABASE_URL.
확인:
kubectl get configmap app-config
kubectl describe pod <pod> | grep -A5 "Environment"수정 YAML:
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret마운트 키 이름이 앱이 읽는 환경변수명과 정확히 일치하는지 꼭 대조하세요.
4) liveness probe 과민 설정
증상: 앱은 멀쩡한데 부팅이 느려서 liveness가 먼저 죽임 → 무한 재시작. (다음 섹션에서 정석 해결)
5) OOM 연계 (Exit 137)
증상: describe에 Reason: OOMKilled, Exit 137.
확인: kubectl describe pod <pod> → Last State Reason. 메모리 limits를 올리거나 앱 힙을 조정해야 합니다. 자세한 내용은 OOMKilled 편을 참고하세요.
probe 튜닝으로 루프 끊기: startupProbe 실전
가장 억울한 CrashLoop은 "앱은 잘 떴는데 liveness에 학살당하는" 패턴입니다. JVM·대형 모델 로딩처럼 부팅이 느린 앱은 initialDelaySeconds만으로 버티기 어렵습니다. 정답은 startupProbe로 부팅 구간을 분리하는 것입니다.
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3 # 부팅과 무관하게 짧게 유지
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
startupProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 30 # 10s * 30 = 최대 300초까지 부팅 허용startupProbe가 성공할 때까지 liveness/readiness는 잠잠합니다. 부팅이 끝나면 그때부터 liveness가 동작하니, "느린 부팅"과 "운영 중 장애"를 깔끔히 분리할 수 있습니다.
probe 권장값 표
| 필드 | liveness | readiness | startupProbe |
|---|---|---|---|
| initialDelaySeconds | 0 (startup이 있으면 불필요) | 5 | 0 |
| periodSeconds | 10 | 5 | 10 |
| failureThreshold | 3 | 3 | 30 (부팅 시간/period) |
| timeoutSeconds | 1~2 | 1~2 | 2~3 |
실무 경험 한마디: Kubernetes 1.30+에서 sidecar가 native initContainer(
restartPolicy: Always)로 정식화되면서, 로그 수집·프록시 사이드카가 메인보다 늦게 떠 발생하던 CrashLoop이 많이 줄었습니다. 다만 GitOps(Argo CD 등)에서 ConfigMap sync가 누락되어 터지는 CrashLoop은 여전히 1위입니다. 저는 배포 후 무조건logs --previous부터 보고, 그 다음 Exit Code를 확인하는 습관을 추천합니다. 추측보다 출력이 빠릅니다.
결론: 재발 방지 체크리스트
루프를 끊었다면 같은 일이 또 일어나지 않게 못을 박읍시다.
- 로컬에서 컨테이너 단독 실행 검증:
docker run --rm <image>로 K8s 밖에서 먼저 떠보기 - ConfigMap/Secret 마운트 키 이름이 앱이 읽는 키와 1:1로 일치하는지 대조
- probe 분리 원칙: 부팅은 startupProbe, 운영 헬스는 liveness, 트래픽 수신은 readiness
- 의존성: DB/캐시는 initContainer
wait-for또는 readinessProbe로 보호 - RESTARTS 모니터링/알람:
kube_pod_container_status_restarts_total메트릭에 알람 설정
Related Errors (시리즈 내부 링크)
- ImagePullBackOff 편 — 이미지를 아예 못 가져올 때
- OOMKilled 편 — Exit 137 메모리 초과 정밀 진단
- PVC Pending 편 — 볼륨 바인딩 실패로 Pod가 안 뜰 때
다음 편 예고
19편에서는 Pod Pending & 스케줄링 실패(노드 리소스 부족, taint/toleration, nodeSelector 미스매치)를 kubectl describe의 FailedScheduling 이벤트부터 추적합니다.
참고: 공식 문서
이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.
자주 묻는 질문 (FAQ)
Q. kubectl logs가 비어 있는데 어떻게 죽은 이유를 보나요?
A. 현재 컨테이너는 막 새로 떠서 로그가 없을 수 있습니다. kubectl logs <pod> --previous(-p)로 직전에 죽은 컨테이너의 로그를 보세요. 대부분의 원인이 여기에 찍힙니다.
Q. Exit Code 137이면 무조건 메모리 부족인가요?
A. 137은 SIGKILL(128+9)을 받았다는 뜻입니다. 대부분 OOMKilled지만, 노드 압박이나 강제 종료로도 발생합니다. kubectl describe pod의 Last State → Reason: OOMKilled 여부로 메모리 문제인지 확정하세요.
Q. 앱은 정상인데 계속 재시작돼요. 무엇부터 볼까요?
A. liveness probe 과민 설정일 확률이 높습니다. 부팅 시간을 startupProbe로 분리하고 liveness의 failureThreshold는 짧게 유지하세요. 부팅이 끝나기 전 liveness가 동작하면 멀쩡한 앱도 무한 재시작됩니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.