- Published on
K8s CrashLoopBackOff 10분 원인별 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 죽고 다시 뜨기를 반복하는 CrashLoopBackOff는 “쿠버네티스가 문제”라기보다, 컨테이너 프로세스가 정상적으로 유지되지 못하는 상황을 의미합니다. 중요한 건 원인을 길게 추측하지 않고, 10분 안에 범주를 좁혀 확정하는 것입니다.
이 글은 다음 전략으로 접근합니다.
- 1분: 어떤 컨테이너가, 왜, 얼마나 자주 죽는지 “사실”부터 수집
- 3분: 종료 코드와 이벤트로 원인 범주 분류
- 6분: 범주별로 재현 가능한 확인 명령과 즉시 조치
아래 루틴을 그대로 따라 하면, 대부분의 CrashLoopBackOff는 10분 안에 원인 후보를 1~2개로 줄일 수 있습니다.
0) CrashLoopBackOff의 의미를 한 줄로 정리
CrashLoopBackOff는 컨테이너가 반복적으로 종료되어 kubelet이 재시작을 지수 백오프(backoff)로 늦추는 상태입니다.
- “왜 죽었는가”는 Pod 상태가 아니라 컨테이너 종료 원인(exit code, signal, OOM, probe 실패 등)에 있습니다.
- 따라서 핵심 데이터는
describe이벤트와logs --previous, 그리고 종료 코드입니다.
1) 10분 진단 타임라인 (복붙용)
1-1. 1분: 대상 확정 (어떤 Pod/컨테이너가 문제인가)
# 네임스페이스에서 크래시 나는 Pod 찾기
kubectl get pod -n <NAMESPACE> -o wide
# 특정 Pod의 컨테이너 상태 요약(재시작 횟수, 상태)
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.state.waiting.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
restartCount가 빠르게 증가하면 “즉시 크래시” 성격lastState.terminated.exitCode가 있으면 종료 코드 기반으로 분류 가능
1-2. 3분: 이벤트와 이전 로그 확보 (가장 중요한 2개)
# 이벤트에서 kubelet이 관측한 이유를 읽는다
kubectl describe pod -n <NAMESPACE> <POD_NAME>
# 직전 실행(크래시 직전)의 로그를 본다
kubectl logs -n <NAMESPACE> <POD_NAME> -c <CONTAINER_NAME> --previous --tail=200
describe의Events:에는Back-off restarting failed container,Unhealthy,OOMKilled등 힌트가 직접 나옵니다.--previous는 CrashLoop에서 거의 필수입니다. 현재 컨테이너는 이미 새로 떠서 로그가 비어 있을 수 있습니다.
1-3. 6분: 종료 코드로 원인 범주 확정
아래 표처럼 exit code는 진단을 매우 빠르게 합니다.
1: 앱 내부 에러, 설정 누락, 외부 의존성 실패 등 “일반 실패”137: OOM kill 또는SIGKILL139: 세그폴트143:SIGTERM(종종 graceful shutdown 실패나 probe/롤링 업데이트와 연관)
# 종료 사유/메시지까지 포함해 한 번 더 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\n"}{.lastState.terminated.reason}{"\n"}{.lastState.terminated.message}{"\n"}{.lastState.terminated.exitCode}{"\n---\n"}{end}'
2) 원인 1: 앱이 “즉시 종료”하는 경우 (exit code 1)
가장 흔한 케이스입니다. 컨테이너는 잘 떴지만, 프로세스가 스스로 종료합니다.
증상
lastState.terminated.exitCode가1또는2logs --previous에 스택트레이스, 설정 파일 없음, 환경 변수 누락, 마이그레이션 실패 등이 보임
10분 내 확인 포인트
- 엔트리포인트/커맨드가 기대대로 실행되는지
kubectl get deploy -n <NAMESPACE> <DEPLOY_NAME> -o yaml | sed -n '1,200p'
command:args:가 이미지의 기본 엔트리포인트를 덮어쓰는지 확인- 쉘 스크립트에서
set -e로 인해 사소한 실패에도 종료되는지 확인
- ConfigMap/Secret 키 누락, 마운트 경로 오류
kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Mounts:/,/Conditions:/p'
- 외부 의존성(DB, 메시지 브로커, OIDC 등) 연결 실패
- DNS나 네트워크 문제면 앱 로그에
timeout,connection refused가 자주 보입니다. - EKS에서 DNS 계열 이슈는 아래 글의 체크리스트가 그대로 도움이 됩니다.
즉시 조치
- “죽지 않게” 만들기보다 왜 죽는지 로그를 남기게 하는 게 우선입니다.
- 앱이 너무 빨리 죽어 디버깅이 어려우면, 임시로 커맨드를 바꿔 컨테이너를 붙잡아 둡니다.
# 임시 디버그: 엔트리포인트 대신 sleep로 컨테이너 유지(운영에서는 주의)
kubectl patch deploy -n <NAMESPACE> <DEPLOY_NAME> --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"]}
]'
그 다음 kubectl exec로 내부에서 실행 파일/환경을 직접 확인합니다.
3) 원인 2: OOMKilled (exit code 137)
CrashLoopBackOff의 두 번째로 흔한 원인입니다. 앱은 실행되지만 메모리를 과도하게 쓰다 커널에 의해 강제 종료됩니다.
증상
describe에OOMKilled또는Reason: OOMKilled- 종료 코드
137 - 재시작이 특정 트래픽/배치 시점에 맞물림
10분 내 확인 포인트
# 컨테이너별 requests/limits 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{.resources}{"\n---\n"}{end}'
# metrics-server가 있다면 현재 사용량 확인
kubectl top pod -n <NAMESPACE> <POD_NAME> --containers
limits.memory가 너무 낮거나,requests.memory가 없어 노드 압박 상황에서 먼저 죽는지 확인- JVM/Node/Python 등 런타임의 기본 메모리 전략이
limits를 고려하지 않아 터지는 경우가 많습니다.
즉시 조치
- 단기:
limits.memory상향, 문제 배치 비활성화, 캐시 크기 축소 - 중기: 힙/워커 수/동시성 튜닝, 메모리 누수 점검
4) 원인 3: Liveness/Readiness Probe 오탐으로 강제 재시작
앱은 살아있지만 livenessProbe가 실패하면 kubelet이 컨테이너를 죽이고 재시작합니다. 이 경우도 CrashLoopBackOff로 보입니다.
증상
describe이벤트에Unhealthy와 함께Liveness probe failed가 반복- 앱 로그에는 치명적 에러가 없을 수 있음
10분 내 확인 포인트
kubectl describe pod -n <NAMESPACE> <POD_NAME> | sed -n '/Liveness:/,/Environment:/p'
initialDelaySeconds가 너무 짧아 “부팅 중”에 죽는지timeoutSeconds가 너무 짧아 일시적 지연에도 실패하는지- HTTP probe의 path/port가 실제 앱과 일치하는지
즉시 조치
- 부팅이 느린 앱은
startupProbe를 도입해 초기 구간을 보호합니다.
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
timeoutSeconds: 2
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
- “헬스 체크는 빠르고 가볍게”가 원칙입니다. DB 쿼리 같은 무거운 체크는 readiness에만 두거나 별도 지표로 빼는 편이 안전합니다.
5) 원인 4: ConfigMap/Secret/볼륨 마운트 문제로 앱이 종료
볼륨 자체가 붙지 않으면 ContainerCreating에서 멈추는 경우가 많지만, 마운트는 됐는데 앱이 기대한 파일이 없거나 권한이 없어서 종료하면 CrashLoopBackOff가 됩니다.
증상
- 로그에
permission denied,no such file or directory,failed to load config등 runAsUser,fsGroup미설정으로 권한 문제
10분 내 확인 포인트
# 볼륨/마운트/보안 컨텍스트 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o yaml | sed -n '1,220p'
securityContext.runAsNonRoot: true인데 이미지가 root 전제면 실패 가능readOnlyRootFilesystem: true인데 앱이/tmp나 작업 디렉터리에 쓰면 즉시 종료
즉시 조치
- 쓰기 경로를
emptyDir로 분리하고 해당 경로만 writable로 제공 fsGroup설정으로 볼륨 권한 정리
6) 원인 5: 노드 디스크/inode 고갈로 런타임이 비정상 종료
노드의 디스크가 가득 차거나 inode가 고갈되면, 이미지 레이어/로그/임시 파일 쓰기에서 실패하며 앱이 죽거나 런타임이 불안정해질 수 있습니다.
증상
- 로그에
no space left on device - 특정 노드에 스케줄된 Pod만 반복 크래시
10분 내 확인 포인트
# Pod가 올라간 노드 확인
kubectl get pod -n <NAMESPACE> <POD_NAME> -o wide
# 해당 노드 이벤트/상태 확인
kubectl describe node <NODE_NAME> | sed -n '/Conditions:/,/Addresses:/p'
노드 디스크/inode 진단은 리눅스 관점에서 접근해야 빨리 풀립니다.
즉시 조치
- 단기: 문제 노드에서 Pod를 다른 노드로 옮기기(노드 cordon/drain)
- 중기: 로그/이미지 GC, ephemeral storage requests/limits 설정, 노드 용량 증설
7) 원인 6: 외부 API 타임아웃이 부팅을 막아 CrashLoop로
앱이 시작 단계에서 외부 API 호출을 동기적으로 수행하고, 타임아웃 처리/재시도가 부적절하면 “부팅 실패”로 이어져 CrashLoop가 됩니다.
증상
- 로그에
ReadTimeout,ConnectTimeout,DEADLINE_EXCEEDED등 - readiness가 뜨기 전에 프로세스가 종료
10분 내 확인 포인트
- 앱이 “시작 시점”에 어떤 외부 의존성을 호출하는지 확인
- 네트워크/DNS가 정상이어도, 재시도 정책이 공격적으로 짜여 있으면 기동 시간이 제한을 초과할 수 있습니다.
재시도 설계 자체가 원인인 경우 아래 글의 패턴이 유용합니다.
즉시 조치
- 시작 단계의 외부 호출을 비동기화하거나, 실패해도 프로세스가 죽지 않게 degrade 모드 도입
startupProbe로 초기 구간 보호 + 재시도 상한/지터 적용
8) 원인 7: “정상 종료”인데도 재시작되는 경우 (프로세스 모델 문제)
컨테이너는 PID 1 프로세스가 살아있어야 합니다. 배치 작업처럼 실행 후 정상 종료(Exit 0)하는 이미지가 Deployment로 돌면, 쿠버네티스는 계속 새로 띄우며 결과적으로 CrashLoop처럼 보일 수 있습니다.
증상
- 종료 코드가
0인데 재시작 - 로그는 정상 완료 메시지
10분 내 확인 포인트
- 워크로드 타입이 맞는지 확인:
Job/CronJob로 돌려야 하는데Deployment로 돌리고 있지 않은지
kubectl get deploy -n <NAMESPACE> <DEPLOY_NAME> -o jsonpath='{.spec.template.spec.restartPolicy}{"\n"}'
Deployment는restartPolicy가 사실상Always전제입니다.
즉시 조치
- 배치성이라면
Job으로 전환 - 상주 프로세스가 필요하다면, 메인 프로세스가 종료되지 않도록 구조 변경
9) 마무리: 10분 루틴 체크리스트
아래 6개만 습관처럼 고정하면, CrashLoop의 대부분은 빠르게 결론이 납니다.
kubectl get pod -o jsonpath로 컨테이너별restartCount와exitCode확인kubectl describe pod에서Events:를 끝까지 읽기kubectl logs --previous로 “죽기 직전 로그” 확보exitCode 137이면 메모리부터 본다(limits,top)Unhealthy면 probe 오탐을 의심하고startupProbe검토- 특정 노드에만 재현되면 노드 디스크/inode/자원 압박을 의심
CrashLoopBackOff는 현상이 강렬해서 원인을 과대해석하기 쉬운데, 실제로는 종료 코드 + 이벤트 + 이전 로그 3종 세트로 대부분 정리됩니다. 이 루틴을 템플릿처럼 돌려서, “추측”이 아니라 “증거”로 복구 시간을 줄이는 쪽이 운영에서 가장 큰 효과를 냅니다.