CrashLoopBackOff 완벽 해결: Pod 무한 재시작 7가지 원인 진단법
배포는 됐는데 Pod가 무한 재시작된다면
kubectl get pod를 쳤더니 STATUS 칸에 CrashLoopBackOff가 떠 있고, RESTARTS 카운트가 5분마다 올라가고 있다면 이 글이 정확히 필요한 순간입니다.
가장 먼저 알아둘 것은 **CrashLoopBackOff는 "상태"가 아니라 "증상"**이라는 점입니다. 컨테이너가 시작 → 크래시 → 쿠버네티스가 재시작 → 또 크래시를 반복하니까, 백오프(지연 재시도) 간격을 두며 다시 시작한다는 뜻일 뿐입니다. 진짜 원인은 컨테이너 안에서 죽은 이유에 있습니다.
이 시리즈의 1편에서 다룬 Pending은 스케줄링 단계의 문제(자원 부족·노드 셀렉터)였고, 2편의 ImagePullBackOff는 이미지를 아예 못 받아오는 문제였습니다. 반면 CrashLoopBackOff는 이미지도 받았고 컨테이너도 떴는데, 실행되자마자 죽는 단계입니다. 즉 한 단계 더 안쪽으로 들어온 셈이죠. 그래서 디버깅 포인트는 "스케줄러"가 아니라 "컨테이너 프로세스의 종료 코드와 로그"입니다.
1분 진단 루틴: describe → logs --previous → Exit Code
원인이 뭐든 시작은 항상 이 3단계입니다. 순서대로만 치면 90%는 1분 안에 범위가 좁혀집니다.
1단계: describe로 Events 확인
kubectl describe pod my-app-7d9f8-abcde맨 아래 Events 섹션이 핵심입니다.
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 2m kubelet Successfully pulled image
Normal Created 2m (x4 over 3m) kubelet Created container app
Normal Started 2m (x4 over 3m) kubelet Started container app
Warning BackOff 30s (x8 over 3m) kubelet Back-off restarting failed containerBack-off restarting failed container는 CrashLoopBackOff의 공통 신호입니다. 이게 보이면 "컨테이너가 시작은 했는데 죽었다"가 확정입니다. 이제 왜 죽었는지를 봐야 합니다.
2단계: logs --previous로 죽은 컨테이너 로그 확보
지금 떠 있는 컨테이너는 또 죽기 직전이라 로그가 비어 있을 수 있습니다. 방금 죽은(이전) 컨테이너의 로그를 봐야 합니다.
kubectl logs my-app-7d9f8-abcde --previous
# 컨테이너가 여러 개면
kubectl logs my-app-7d9f8-abcde -c app --previous--previous(축약 -p)를 빼먹어서 "로그가 안 나온다"며 헤매는 경우가 정말 많습니다. 크래시 디버깅의 절반은 이 옵션 하나에 달려 있습니다.
3단계: Exit Code 확인
describe 출력의 Last State 블록에서 종료 코드를 읽습니다.
Last State: Terminated
Reason: OOMKilled
Exit Code: 137kubectl get pod의 RESTARTS 카운트도 같이 보세요. 빠르게 치솟으면 즉시 크래시, 한참 떠 있다가 죽으면 메모리 누수나 probe 문제일 가능성이 큽니다.
에러 메시지 → 원인 매핑 표
| 로그/Events 메시지 | 유력 원인 | 바로 갈 섹션 |
|---|---|---|
Back-off restarting failed container | 공통 신호 (컨테이너 크래시) | 전부 |
OOMKilled / Exit 137 | 메모리 한도 초과 | 원인 2 |
Error: secret "xxx" not found / missing env | 환경변수·시크릿 누락 | 원인 3 |
Liveness probe failed | 프로브 설정 오류 | 원인 4 |
connection refused / dial tcp ... | 의존 서비스(DB) 미준비 | 원인 5 |
no such file or directory | 마운트 경로/command 오류 | 원인 6·7 |
| Exit 1 + 스택트레이스 | 앱 일반 예외 | 원인 1 |
원인별 진단·해결 7선
원인 1. 애플리케이션 예외 / 엔트리포인트 오류 (Exit 1)
가장 흔한 케이스. 앱 코드가 시작하자마자 예외를 던지고 죽습니다.
kubectl logs <pod> --previous # 스택트레이스 확인로그에 NullPointerException, Cannot find module, panic: 같은 메시지가 그대로 찍힙니다. 이건 인프라 문제가 아니라 앱 버그이므로, 로그가 가리키는 코드/설정을 고치는 게 정답입니다. 인프라 담당자라면 로그를 캡처해 개발팀에 그대로 넘기면 됩니다.
원인 2. OOMKilled (Exit 137)
kubectl describe pod <pod> | grep -A2 "Last State" # Reason: OOMKilled 확인
kubectl top pod <pod> # 실사용 메모리 확인137 = 128 + 9, 즉 SIGKILL입니다. 메모리 limit을 초과해 커널이 강제 종료한 것입니다.
Before (한도가 너무 빡빡)
resources:
limits:
memory: "128Mi"
requests:
memory: "128Mi"After (실사용 기반 상향)
resources:
requests:
memory: "256Mi" # 평상시 사용량
limits:
memory: "512Mi" # 피크 여유분근본 해결책은 kubectl top pod로 실제 사용량을 측정한 뒤 limit을 맞추는 것입니다. OOM이 잦은 워크로드라면 VPA(Vertical Pod Autoscaler)로 requests/limits를 자동 튜닝하는 것도 최근 많이 쓰는 방법입니다.
원인 3. 환경변수 · 시크릿 누락
kubectl logs <pod> --previous # "secret not found" / "env XXX is undefined"
kubectl get secret db-credentials # 시크릿 존재 여부
kubectl get secret db-credentials -o yaml # 키 이름 확인Before (참조하는 시크릿이 없거나 이름 오타)
envFrom:
- secretRef:
name: db-credential # 's' 누락After
envFrom:
- secretRef:
name: db-credentials근본 해결책: 시크릿/ConfigMap을 Pod보다 먼저 배포하고, 이름과 키를 kubectl get secret으로 대조하세요. 같은 네임스페이스에 있는지도 꼭 확인합니다.
원인 4. Liveness Probe 오류
컨테이너 자체는 멀쩡한데, 앱이 다 뜨기도 전에 probe가 실패해서 kubelet이 죽이는 경우입니다.
Warning Unhealthy kubelet Liveness probe failed: HTTP probe failed with statuscode: 500
Normal Killing kubelet Container failed liveness probe, will be restartedBefore (기동 시간을 안 줌)
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 1
failureThreshold: 1After (기동 여유 + 임계치 완화)
startupProbe: # 1.29+ 권장: 기동 전용 프로브 분리
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
failureThreshold: 3
periodSeconds: 10근본 해결책: 기동이 느린 앱은 startupProbe로 분리하고, liveness는 "진짜로 죽었을 때만" 발동하도록 보수적으로 설정합니다. readiness/liveness/startup을 역할별로 나누는 게 1.29+ 권장 패턴입니다.
원인 5. 의존 서비스(DB) 연결 실패
kubectl logs <pod> --previous # "connection refused" / "dial tcp 10.x:5432"앱이 DB·Redis가 준비되기 전에 떠서 연결 실패로 죽는 케이스입니다.
해결: initContainer로 의존성 대기
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c',
'until nc -z postgres 5432; do echo waiting; sleep 2; done']근본적으로는 앱에 재시도(backoff retry) 로직을 넣는 것이 가장 견고합니다. initContainer는 기동 순서를 보장하지만, 운영 중 DB가 잠깐 끊겼을 때의 복원력은 앱 레벨 재시도가 책임집니다.
원인 6. ConfigMap 마운트 경로 오류
kubectl logs <pod> --previous # "no such file or directory: /config/app.yaml"
kubectl describe pod <pod> # Volumes / Mounts 확인Before (subPath 없이 디렉터리째 덮어 기존 파일이 사라짐)
volumeMounts:
- name: config
mountPath: /app/config/app.yaml # 파일을 디렉터리로 마운트After (subPath로 단일 파일만 주입)
volumeMounts:
- name: config
mountPath: /app/config/app.yaml
subPath: app.yaml
volumes:
- name: config
configMap:
name: app-config근본 해결책: 단일 파일을 주입할 땐 subPath를 쓰고, mountPath가 앱이 실제로 읽는 경로와 일치하는지 확인합니다.
원인 7. 잘못된 command / args
이미지 엔트리포인트를 잘못 덮어쓰면 exec: "xxx": executable file not found가 뜹니다.
Before
command: ["python3"]
args: ["app.py"] # 작업 디렉터리에 app.py가 없음 → 즉시 종료After
command: ["python3"]
args: ["/app/main.py"]근본 해결책: 이미지의 기본 ENTRYPOINT/CMD를 확인하고, 꼭 필요할 때만 override 하세요.
Exit Code 해석 표
| Exit Code | 시그널 | 의미 | 먼저 볼 곳 |
|---|---|---|---|
| 0 | - | 정상 종료인데 재시작 | restartPolicy·엔트리포인트가 데몬으로 안 떠 있음 |
| 1 | - | 앱 일반 예외 | logs --previous 스택트레이스 |
| 137 | SIGKILL | OOM 또는 강제 종료 | memory limit, kubectl top |
| 139 | SIGSEGV | 세그멘테이션 폴트 | 네이티브 라이브러리/아키텍처(arm vs amd) |
| 143 | SIGTERM | 정상 종료 신호 | graceful shutdown 처리, 외부 종료 요인 |
Exit 0인데 재시작한다면 십중팔구 "한 번 실행하고 끝나는 스크립트"를 Deployment로 띄운 경우입니다. 일회성 작업이면 Job으로 바꾸세요.
로그가 아예 안 남는 즉시 종료 디버깅
가장 짜증나는 케이스. --previous를 쳐도 로그가 비어 있고, 컨테이너가 너무 빨리 죽어 exec도 못 들어가는 상황입니다. 이럴 땐 컨테이너를 일부러 살려두고 내부를 직접 봅니다.
# 임시로 엔트리포인트를 sleep으로 덮어 컨테이너를 살려둠
command: ["sleep", "3600"]이 상태로 배포한 뒤 내부에 들어가 손으로 실행해 봅니다.
kubectl exec -it <pod> -- sh
# 안에서 직접 실행해 진짜 에러 메시지 확인
/app/entrypoint.sh원본 이미지를 건드리기 싫다면 ephemeral container를 붙이는 kubectl debug도 좋습니다.
kubectl debug -it <pod> --image=busybox --target=app -- sh실무 경험 한마디: 저는 새벽 장애 대응에서 CrashLoopBackOff의 70% 이상이 결국 "시크릿 이름 오타"와 "OOMKilled" 둘 중 하나였습니다. 그래서 호출 받으면 무조건
describe로 Exit Code부터 봅니다. 137이면 메모리, secret 메시지면 설정, 그 외면logs --previous로 직행. 이 분기 하나로 평균 대응 시간이 절반으로 줄었습니다.
결론: 진단 체크리스트
kubectl get pod→ RESTARTS 카운트와 STATUS 확인kubectl describe pod→ Events의Back-off restarting...와 Exit Code 확인kubectl logs <pod> --previous→ 죽은 컨테이너 로그 확보- Exit Code로 분기: 137→메모리, 1→앱 예외, secret/env→설정, probe→프로브
- 로그가 없으면
command: sleepoverride 또는kubectl debug로 내부 진입
이미지를 못 받아오는 ImagePullBackOff는 2편, 스케줄링 단계에서 막히는 Pending은 1편을 참고하세요. 컨테이너는 떴는데 외부에서 접속이 안 된다면, 다음 4편 Service/Endpoint 연결 실패 트러블슈팅에서 이어집니다.
참고: 공식 문서
이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.
자주 묻는 질문 (FAQ)
Q. kubectl logs --previous를 쳤는데 "previous terminated container not found"가 나옵니다.
A. 아직 컨테이너가 재시작되지 않았거나(첫 크래시 직후), 노드가 이전 컨테이너를 정리한 경우입니다. 잠깐 기다렸다가 RESTARTS가 올라간 뒤 다시 치거나, kubectl describe의 Last State 블록에서 종료 이유를 확인하세요.
Q. Exit Code 137인데 kubectl top pod로 보면 메모리가 limit보다 낮습니다. 왜 OOM이 날까요?
A. 순간 피크에서 limit을 넘겼다가 죽은 뒤 측정된 값일 수 있습니다. top은 현재값만 보여주므로, Prometheus 등으로 container_memory_working_set_bytes의 피크를 확인하고 limit을 그 위로 올리세요. 137이 SIGKILL이라는 점에서 외부 강제 종료(예: 노드 자원 압박)도 의심해볼 수 있습니다.
Q. CrashLoopBackOff 상태에서 재시작 간격이 점점 길어지는데 정상인가요? A. 정상입니다. 쿠버네티스는 10초부터 시작해 최대 5분까지 지수적으로 백오프 간격을 늘립니다. 원인을 고쳐 컨테이너가 정상 기동하면 카운터가 리셋됩니다. 간격이 길다고 장애가 심한 건 아니니, 간격보다 종료 코드와 로그에 집중하세요.
AI 도구는 자료 조사와 초안 작성의 보조 수단으로 사용될 수 있습니다. Nodelog는 공개 전 내용과 출처를 검토하고, 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서를 함께 확인하도록 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.