- Published on
K8s CrashLoopBackOff 즉시 원인 찾는 법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff는 쿠버네티스가 컨테이너를 재시작하다가 백오프(backoff)로 간격을 늘리는 상태입니다. 중요한 포인트는 CrashLoopBackOff 자체가 원인이 아니라, 컨테이너가 왜 종료됐는지(Exit Code, Signal, OOMKilled, Probe 실패, 이미지/권한/설정 오류 등) 를 빠르게 특정하는 것이 핵심이라는 점입니다.
운영에서 “즉시 원인 찾기”는 보통 다음 3가지만 확보하면 됩니다.
describe이벤트에서 무슨 이유로 재시작하는지lastState.terminated에서 Exit Code / Reason / Signal--previous로그에서 죽기 직전 무엇을 하다 죽었는지
아래 순서대로 따라 하면, 재현 환경이 없어도 대부분 5분 내로 원인 범위를 좁힐 수 있습니다.
1) 30초 컷: Pod 이벤트부터 본다
가장 먼저 해야 할 일은 kubectl describe pod로 이벤트를 보는 것입니다. CrashLoopBackOff의 1차 단서는 대부분 이벤트에 그대로 찍힙니다.
# 네임스페이스 지정
kubectl -n <ns> describe pod <pod>
MDX 빌드 에러를 피하기 위해 위의 <ns>, <pod> 같은 토큰은 인라인 코드로 감쌌습니다.
여기서 특히 아래 라인을 찾습니다.
Back-off restarting failed containerReadiness probe failed/Liveness probe failedOOMKilledError: failed to start container(이미지, 권한, 마운트 문제)
이벤트만으로도 “프로브 때문에 재시작인지”, “앱이 즉시 크래시인지”, “노드/런타임 문제인지”가 분리됩니다.
자주 보이는 이벤트 패턴
- 프로브 실패 반복: 컨테이너는 살아있지만 헬스체크가 계속 실패해서 재시작
- Exit Code 1, 2, 137, 139 등: 앱 프로세스 자체가 종료
- CreateContainerConfigError / ImagePullBackOff: CrashLoopBackOff로 보이기 전에 다른 상태가 먼저 찍히는 경우도 있음
2) 1분 컷: Exit Code와 Reason을 구조적으로 확인
describe는 사람이 읽기 좋지만, 빠르게 핵심만 뽑으려면 JSONPath가 강력합니다.
kubectl -n <ns> get pod <pod> -o jsonpath='{.status.containerStatuses[*].name}{"\n"}{.status.containerStatuses[*].state}{"\n"}{.status.containerStatuses[*].lastState.terminated}{"\n"}'
여기서 봐야 하는 필드:
lastState.terminated.exitCodelastState.terminated.reasonlastState.terminated.signallastState.terminated.finishedAtstate.waiting.reason가CrashLoopBackOff인지
Exit Code 빠른 해석표
1/2: 앱 설정 오류, 인자/환경변수 누락, 마이그레이션 실패 등 “일반적인 실패”126: 실행 권한 문제(바이너리 실행 불가)127: 엔트리포인트/커맨드 없음(PATH 문제)137: SIGKILL로 종료. OOMKilled 또는 노드가 강제 종료했을 가능성 큼139: SIGSEGV(세그폴트)
reason이 OOMKilled로 찍히면 거의 게임 끝입니다. 메모리 제한과 실제 사용량을 맞추거나, 앱의 메모리 급증 원인을 찾아야 합니다.
3) 2분 컷: --previous 로그로 “죽기 직전”을 본다
CrashLoopBackOff에서 가장 가치 있는 로그는 현재 컨테이너 로그가 아니라 직전 크래시 인스턴스 로그입니다.
# 단일 컨테이너 Pod
kubectl -n <ns> logs <pod> --previous
# 멀티 컨테이너 Pod
kubectl -n <ns> logs <pod> -c <container> --previous
여기서 흔히 나오는 즉시 원인:
- 설정 파일 경로 오류, 시크릿/컨피그맵 키 누락
- 외부 의존성(DB, Redis, STS, S3, DNS) 초기 연결 실패 후 즉시 종료
- 마이그레이션 실패로 프로세스 종료
- 바인딩 실패(
Address already in use, 포트 권한 문제)
외부 의존성(특히 AWS 자격증명, STS, S3, DNS) 때문에 초기화 단계에서 죽는 경우가 많습니다. EKS라면 IRSA/IMDS 이슈도 매우 흔하니, 401 또는 메타데이터 접근 실패가 보이면 아래 글도 같이 확인해보세요.
4) 프로브가 원인인지 10초 만에 판별하는 법
CrashLoopBackOff의 “가짜 원인” 1위가 프로브입니다. 앱은 정상인데 프로브 설정이 공격적으로 되어 있으면, 컨테이너가 뜨자마자 재시작 루프에 들어갑니다.
describe pod에서 아래를 확인합니다.
Liveness probe failed이벤트가 반복되는가initialDelaySeconds가 너무 짧지 않은가timeoutSeconds가 너무 짧지 않은가failureThreshold가 너무 낮지 않은가
즉시 조치(원인 분리용)
원인 분리를 위해서만 일시적으로 liveness를 완화하거나 제거하는 방법이 있습니다.
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 2
periodSeconds: 10
failureThreshold: 6
이렇게 했는데도 계속 죽는다면 “프로브”가 아니라 “프로세스 크래시/종료” 쪽으로 봐야 합니다.
5) OOMKilled(Exit 137)면 무엇을 더 봐야 하나
OOMKilled는 컨테이너 메모리 제한을 넘었을 때 커널이 프로세스를 강제 종료하는 전형적인 케이스입니다.
즉시 확인:
kubectl -n <ns> describe pod <pod> | sed -n '/Containers:/,/Conditions:/p'
여기서 Limits / Requests 메모리 값을 확인하고, 애플리케이션 특성과 맞는지 봅니다.
추가로 노드 레벨 압박인지 확인하려면:
kubectl describe node <node> | sed -n '/Allocated resources:/,/Events:/p'
- 노드 자체가 메모리 압박이면 여러 Pod가 연쇄적으로 불안정해질 수 있습니다.
- 특정 Pod만 OOM이면 해당 워크로드의 메모리 사용 패턴(캐시, 배치, 대용량 응답, JVM 힙 등)을 점검합니다.
6) CreateContainerConfigError / 마운트/시크릿 문제로 즉시 죽는 케이스
CrashLoopBackOff처럼 보이지만 실제로는 컨테이너가 제대로 시작도 못 하는 경우가 있습니다.
대표 원인:
secretKeyRef키 이름 오타configMapKeyRef키 누락- 볼륨 마운트 경로/권한 문제
- 서비스어카운트/이미지풀 시크릿 누락
이때는 이벤트에 답이 있습니다.
kubectl -n <ns> get events --sort-by='.lastTimestamp' | tail -n 30
이벤트에 MountVolume.SetUp failed 같은 문구가 있으면 애플리케이션 로그를 보기 전에 쿠버네티스 리소스 정의부터 고쳐야 합니다.
7) 엔트리포인트/커맨드 문제(Exit 126/127) 빠르게 잡기
도커 이미지 자체는 잘 빌드됐는데, 런타임에서 실행 파일을 못 찾거나 권한이 없어 죽는 케이스입니다.
exitCode: 127이면 커맨드가 존재하지 않을 확률이 큽니다.exitCode: 126이면 실행 권한 또는 아키텍처 불일치 가능성이 있습니다.
즉시 확인할 것:
spec.containers[].command/args가 이미지 내부와 맞는지- 셸 스크립트라면
#!/bin/sh경로가 존재하는지 - 파일에 실행 권한이 있는지
원인 분리를 위해 임시로 엔트리포인트를 바꿔 컨테이너에 들어가 확인할 수도 있습니다.
kubectl -n <ns> patch deploy <deploy> --type='json' \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["/bin/sh","-c","sleep 3600"]}]'
그 다음:
kubectl -n <ns> exec -it <pod> -- /bin/sh
컨테이너 내부에서 실제 실행 경로와 권한을 확인합니다.
8) “로그가 비어있다”면: stdout 이전에 죽는 것이다
kubectl logs가 비어 있으면 보통 아래 중 하나입니다.
- 프로세스가 stdout을 남기기 전에 즉시 크래시
- 애플리케이션 로그가 파일로만 찍히고 stdout으로 안 나감
- 런타임이 시작 전에 막힘(이미지/마운트/권한)
이때는 --previous와 이벤트가 거의 유일한 단서입니다. 또한 Ingress나 ALB 5xx로 먼저 관측되는 경우도 많습니다. 외부에서 502/504가 보이는데 Pod 로그가 비어 있다면, 트래픽이 Pod까지 도달했는지(헬스체크, 타겟 그룹, readiness)부터 분리해야 합니다.
9) 디버깅용 임시 컨테이너: Ephemeral Container 활용
프로세스가 너무 빨리 죽어서 exec가 불가능하면, Ephemeral Container로 네트워크/DNS/파일을 확인할 수 있습니다.
kubectl -n <ns> debug -it <pod> --image=busybox --target=<container> -- sh
여기서 즉시 확인할 체크:
- DNS:
nslookup <service> - 라우팅:
wget -S -O- http://<service>:<port>/health - 환경변수:
env | sort - 마운트:
ls -al /path
DNS 타임아웃이 의심되면 CoreDNS 및 노드 레벨 이슈까지 확장해야 합니다.
10) 운영에서 바로 쓰는 “즉시 원인 찾기” 커맨드 세트
아래 6줄을 복사해 두면, 대부분의 CrashLoopBackOff를 빠르게 분류할 수 있습니다.
kubectl -n <ns> get pod <pod> -o wide
kubectl -n <ns> describe pod <pod>
kubectl -n <ns> logs <pod> --previous
kubectl -n <ns> get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
kubectl -n <ns> get events --sort-by='.lastTimestamp' | tail -n 50
kubectl -n <ns> get deploy <deploy> -o yaml | sed -n '1,200p'
describe와events로 “쿠버네티스가 관측한 실패”를 확보lastState.terminated로 “종료 코드/시그널”을 확보--previous로 “앱이 말한 마지막 한 마디”를 확보
이 3가지를 합치면, 원인을 대개 다음 중 하나로 즉시 귀결시킬 수 있습니다.
- 프로브 설정 문제
- OOMKilled(메모리 제한/누수/버스트)
- 엔트리포인트/권한/파일 누락
- 시크릿/컨피그맵/볼륨 마운트 오류
- 외부 의존성(DNS, STS, S3, DB) 초기화 실패
마무리: CrashLoopBackOff는 “증상”, 답은 Exit Code에 있다
CrashLoopBackOff를 빨리 끝내는 요령은 복잡한 추측을 줄이고, 관측 가능한 사실을 순서대로 수집하는 것입니다.
- 이벤트로 큰 분기(프로브 vs 크래시 vs 런타임)를 나누고
lastState.terminated로 종료 코드를 확정한 뒤--previous로그로 애플리케이션 레벨 원인을 못 박습니다.
여기까지 해도 원인이 외부 의존성(특히 EKS의 DNS/인증/네트워크)로 남는다면, 위에 링크한 IRSA/IMDS, CoreDNS 점검 글을 함께 따라가면 “CrashLoopBackOff의 진짜 원인”까지 빠르게 도달할 수 있습니다.