- Published on
K8s CrashLoopBackOff 원인별 10분 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 죽고 다시 뜨고, 또 죽는 루프는 운영자에게 가장 불쾌한 신호입니다. Kubernetes에서 CrashLoopBackOff는 컨테이너가 반복적으로 비정상 종료하고 kubelet이 재시작을 시도하되, 재시작 간격을 점점 늘리는(backoff) 상태를 의미합니다. 중요한 점은 CrashLoopBackOff 자체가 원인이 아니라 증상이라는 것입니다.
이 글은 “10분 안에 원인 범위를 좁히는” 것을 목표로 합니다. 핵심은 딱 4가지 데이터를 빠르게 확보하는 것입니다.
- 이벤트(Event): 무엇 때문에 재시작이 트리거됐는가
- 종료코드(Exit Code) / 종료사유(Reason): 프로세스가 왜 죽었는가
- 로그(Log): 앱이 죽기 직전에 무슨 말을 했는가
- 리소스/프로브/설정: 죽게 만든 환경적 조건은 무엇인가
> 참고: OOM이 의심된다면 리눅스 관점의 원인/방지도 함께 보면 좋습니다. 리눅스 OOM Killer로 프로세스 죽음 진단·방지
0~2분: “가장 먼저” 실행할 5개 명령
아래 명령만으로도 절반 이상은 방향이 잡힙니다.
# 1) 상태/재시작 횟수/노드 확인
kubectl -n <ns> get pod <pod> -o wide
# 2) 이벤트: BackOff, OOMKilled, FailedMount, Unhealthy 등 키워드가 바로 나옴
kubectl -n <ns> describe pod <pod>
# 3) 직전 크래시 로그(가장 중요)
kubectl -n <ns> logs <pod> --previous --all-containers
# 4) 현재 로그(죽기 직전/직후 비교)
kubectl -n <ns> logs <pod> --all-containers --tail=200
# 5) 종료코드/Reason을 JSONPath로 빠르게
kubectl -n <ns> get pod <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\t"}{.restartCount}{"\n"}{end}'
10분 진단의 분기 기준(치트시트)
reason=OOMKilled또는 exitCode137→ 메모리 부족(OOM)- exitCode
1/2+ 앱 로그에 stacktrace → 앱 설정/의존성/마이그레이션 문제 FailedMount,MountVolume.SetUp failed→ 볼륨/시크릿/권한/CSIUnhealthy이벤트 +Readiness/Liveness probe failed→ 프로브 설정/부팅 지연/엔드포인트CreateContainerConfigError,ImagePullBackOff→ 이미지/환경변수/시크릿/레지스트리- 노드 이벤트에
Evicted/DiskPressure→ 노드 자원 압박
2~10분: 원인별 “즉시” 진단 루틴
아래는 CrashLoopBackOff의 대표 원인들을 증상 → 확인 포인트 → 1차 조치 순으로 정리한 실전 루틴입니다.
1) OOMKilled (메모리 부족) — exitCode 137
증상
describe에OOMKilled또는Container killed by OOM문구- 종료코드 137(= SIGKILL)
- 재시작이 일정 주기로 반복(트래픽/배치 타이밍과 연동되기도)
1분 확인
kubectl -n <ns> describe pod <pod> | sed -n '/State:/,/Conditions:/p'
kubectl -n <ns> top pod <pod>
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[*].resources}'
1차 조치(가장 빠른 순)
- 메모리 limit 상향 또는 limit 제거(권장 X, 임시)
- JVM/Node/Python 등 런타임 힙 제한을 limit에 맞추기
- 메모리 누수 의심 시, 재현 트래픽/배치 구간에서 프로파일링
예: Deployment 메모리 상향 패치
kubectl -n <ns> patch deploy <deploy> --type='json' -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"1024Mi"},
{"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"512Mi"}
]'
> OOM이 반복되면 “앱이 진짜로 메모리를 더 필요로 하는지”와 “limit에 맞춘 런타임 설정인지”를 분리해서 봐야 합니다. 자세한 원인 분석은 리눅스 OOM Killer로 프로세스 죽음 진단·방지도 같이 참고하세요.
2) Liveness/Readiness Probe 실패로 강제 재시작
증상
- 이벤트에
Unhealthy,Liveness probe failed반복 - 앱 자체는 살아있는데 kubelet이 죽였다가 재시작
- 부팅이 느린 서비스(마이그레이션/캐시 워밍업)에서 흔함
2분 확인
kubectl -n <ns> describe pod <pod> | sed -n '/Liveness:/,/Events:/p'
# 프로브 엔드포인트를 포트포워딩으로 직접 확인
kubectl -n <ns> port-forward pod/<pod> 18080:8080
curl -i http://127.0.0.1:18080/healthz
1차 조치
- startupProbe 도입(부팅 구간 보호)
- liveness의
initialDelaySeconds,timeoutSeconds,failureThreshold조정 - readiness는 트래픽 차단용, liveness는 “진짜 죽었을 때만”
예: startupProbe 추가 예시
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 2
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
3) 설정/환경변수/시크릿 오류 (exitCode 1, CreateContainerConfigError)
증상
- 앱 로그에 “필수 env 없음”, “config parse error”, “cannot read secret”
- 또는 컨테이너가 뜨기 전
CreateContainerConfigError
2분 확인
kubectl -n <ns> describe pod <pod> | sed -n '/Environment:/,/Mounts:/p'
# secret/configmap 존재 여부
kubectl -n <ns> get secret,cm | grep -E '<secret-name>|<cm-name>'
# 키 누락 확인
kubectl -n <ns> get secret <secret-name> -o jsonpath='{.data}'
1차 조치
- Secret/ConfigMap 키 이름 오타, base64 인코딩 여부 확인
envFrom사용 시 예상치 못한 키 충돌 점검- 애플리케이션이 “필수 값 누락 시 즉시 종료”하도록 되어 있으면 CrashLoop가 빠르게 발생
4) 이미지/엔트리포인트/권한 문제 (exec format error, permission denied)
증상
- 로그에
exec format error(아키텍처 불일치: arm64/amd64) permission denied(실행 비트/USER 권한)no such file or directory(ENTRYPOINT 경로)
2분 확인
kubectl -n <ns> describe pod <pod> | sed -n '/Image:/,/Environment:/p'
# 노드 아키텍처 확인
kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.architecture}{"\n"}{end}'
1차 조치
- 멀티아치 이미지(manifest list)로 빌드/푸시
- Dockerfile에서
ENTRYPOINT/CMD경로, 실행권한(chmod +x) 확인 runAsNonRoot사용 시 바이너리/디렉터리 권한 재점검
5) 볼륨 마운트 실패 (FailedMount, permission)
증상
- 이벤트에
FailedMount,MountVolume.SetUp failed - CSI/IAM 권한 부족, PVC Pending, Secret volume 누락 등
2분 확인
kubectl -n <ns> describe pod <pod> | sed -n '/Volumes:/,/QoS Class:/p'
kubectl -n <ns> get pvc
kubectl -n <ns> describe pvc <pvc>
1차 조치
- PVC가 Bound인지, StorageClass/CSI 드라이버 정상인지 확인
- Secret/ConfigMap volume이면 리소스 존재/키 확인
- EKS라면 IRSA/IAM 권한 문제로도 파생될 수 있음(스토리지/컨트롤러)
6) 의존 서비스 불가로 앱이 즉시 종료 (DB/Redis/Kafka/DNS)
증상
- 앱 로그:
ECONNREFUSED,connection timeout,getaddrinfo ENOTFOUND - readiness 실패가 아니라 “앱이 부팅 중 연결 실패 시 종료”하는 패턴
3분 확인
- 같은 네임스페이스에서 임시 디버그 파드로 네트워크 확인
kubectl -n <ns> run net-debug --rm -it --image=busybox:1.36 --restart=Never -- sh
# DNS
nslookup <service-name>
# TCP 연결
wget -S -O- http://<service-name>:<port> 2>&1 | head
- 서비스/엔드포인트 확인
kubectl -n <ns> get svc <service>
kubectl -n <ns> get endpoints <service>
1차 조치
- 앱을 “의존 서비스 준비 전에는 재시도(backoff)하며 살아있게” 수정(권장)
- initContainer로 의존성 체크 후 본 컨테이너 시작
- 클러스터 DNS/네트워크 정책(NetworkPolicy) 차단 여부 확인
> EKS에서 네트워크/엔드포인트 계열 이슈는 502/504, TLS 실패로도 이어집니다. 인그레스/ALB까지 연쇄 장애가 의심되면 EKS ALB Ingress 502/504 - TLS 핸드셰이크 실패 진단도 같이 보면 원인 범위를 더 빨리 줄일 수 있습니다.
7) 애플리케이션 마이그레이션/배치 작업이 매번 실행되어 실패
증상
- 컨테이너 시작 시 DB migration 수행 → 실패 → 종료 → 재시작 시 또 migration
- 로그에 migration tool 출력이 반복
2분 확인
kubectl -n <ns> logs <pod> --previous --all-containers | tail -n 200
1차 조치
- migration을 Job/InitContainer로 분리(한 번만 실행)
- 실패 시 종료 대신 재시도/알람 후 대기하는 전략 고려
8) 노드 자원 압박/퇴거(Eviction), 디스크 부족
증상
Evicted,DiskPressure,ImageGCFailed등 노드 이벤트- 특정 노드에만 스케줄링되면 반복 발생
2분 확인
kubectl describe pod <pod> -n <ns> | grep -E 'Node:|Evicted|DiskPressure|Reason'
kubectl describe node <node> | sed -n '/Conditions:/,/Addresses:/p'
1차 조치
- 노드 디스크/인오드 정리, 로그 폭증 확인
- requests/limits를 현실화(QoS 개선)
- 문제 노드 cordon/drain 후 교체
9) 종료 시그널 처리 실패(Graceful shutdown 미구현)로 재기동 루프 유발
증상
- 배포/스케일링/노드 드레인 시 SIGTERM 처리 못하고 즉시 종료
- preStop 훅/terminationGracePeriodSeconds 부족
확인 및 조치
terminationGracePeriodSeconds: 60
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
- 앱에서 SIGTERM 핸들러 구현(서버 close, 큐 flush)
10) “진단 속도”를 올리는 디버깅 패턴 3가지
A. --previous 로그를 습관화
CrashLoop에서는 현재 로그보다 직전 종료 로그가 결정적입니다.
kubectl -n <ns> logs <pod> --previous
B. Ephemeral Container로 살아있는 순간에 내부 확인
컨테이너가 너무 빨리 죽으면, ephemeral container로 내부 파일/환경을 확인합니다.
kubectl -n <ns> debug -it pod/<pod> --image=busybox:1.36 --target=<container-name>
C. 재시작 지연을 “의도적으로” 늘려 관찰
앱이 너무 빨리 죽어 관찰이 어려우면, 임시로 커맨드를 바꿔 쉘 대기 상태로 올려 원인을 확인합니다(운영 환경에서는 주의).
kubectl -n <ns> patch deploy <deploy> --type='json' -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["/bin/sh","-c"]},
{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["sleep 3600"]}
]'
마무리: 10분 내 결론을 내는 순서
CrashLoopBackOff를 빠르게 끝내려면 “추측”보다 “정렬된 증거”가 필요합니다.
describe pod이벤트에서 1차 분류logs --previous로 직전 크래시 메시지 확보- 종료코드/Reason으로 OOM/시그널/에러 구분
- 프로브/리소스/볼륨/시크릿/의존성 순서로 체크
- 임시 조치(리소스 상향, startupProbe, 설정 수정) 후 재발 방지(구조 개선)
이 루틴을 팀의 런북으로 고정해두면, CrashLoopBackOff는 “장애”가 아니라 “디버깅 가능한 이벤트”로 바뀝니다.