- Published on
K8s CrashLoopBackOff 원인별 진단 체크리스트
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Kubernetes에서 CrashLoopBackOff는 단일 원인이 아니라 **"컨테이너 프로세스가 종료됨" + "Kubelet이 재시작을 반복"**하는 결과 상태입니다. 즉, 문제의 핵심은 항상 컨테이너가 왜 종료되는가로 귀결됩니다.
운영에서 중요한 건 원인 추정이 아니라 재현 가능한 관찰 순서입니다. 아래 체크리스트는 "증상 확인 → 종료 이유 분류 → 원인별 조치" 흐름으로 구성했습니다.
참고로 이미지 자체를 못 받아서 뜨는 ImagePullBackOff는 CrashLoopBackOff와 결이 다릅니다. 먼저 이미지 풀 이슈를 배제하려면 Kubernetes ImagePullBackOff·ErrImagePull 해결 체크리스트도 함께 확인하세요.
1) 3분 안에 상태를 고정하는 기본 진단 루틴
1-1. 이벤트와 종료 코드를 먼저 본다
아래 3개 명령으로 대부분의 방향이 결정됩니다.
# 1) Pod 상태 요약
kubectl get pod -n <namespace> <pod-name> -o wide
# 2) 이벤트(Back-off, OOMKilled, probe fail 등 단서)
kubectl describe pod -n <namespace> <pod-name>
# 3) 직전 종료 로그(재시작 루프에서는 --previous가 핵심)
kubectl logs -n <namespace> <pod-name> -c <container-name> --previous
describe에서 특히 아래 필드를 집중해서 봅니다.
State: Waiting/Reason: CrashLoopBackOffLast State: Terminated/Reason: Error | OOMKilled | CompletedExit Code: 1 | 137 | 139 ...Events:섹션의Back-off restarting failed container,Killing container,Unhealthy메시지
1-2. 컨테이너가 너무 빨리 죽으면 "디버그용"으로 잠깐 붙잡기
프로세스가 즉시 종료되면 exec로 들어갈 시간도 없습니다. 이때는 임시로 커맨드를 바꿔서 컨테이너를 살려두고 파일/환경변수를 확인합니다.
kubectl patch deployment -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"]}
]'
원인 파악 후 반드시 원복하세요.
2) 원인 분류: 종료 이유별 "첫 번째 가설"
CrashLoopBackOff는 보통 아래 범주로 나뉩니다.
- 애플리케이션 즉시 종료(설정/의존성/마이그레이션/권한)
- 프로브 실패(liveness/readiness/startup)
- 리소스 문제(OOM, CPU starvation)
- 런타임/노드/스토리지/네트워크 이슈
- 정상 종료인데 재시작 정책 때문에 루프
이제 각 범주별 체크리스트를 봅니다.
3) 애플리케이션 즉시 종료: 설정, 파일, 의존성
3-1. 환경변수 누락 또는 잘못된 값
가장 흔한 패턴은 "필수 환경변수 미설정"으로 프로세스가 exit 1 하는 경우입니다.
체크:
kubectl get deploy -n <namespace> <deploy-name> -o yaml | sed -n '1,200p'
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Environment:/,/Mounts:/p'
전형적 로그:
Missing required env var ...failed to bind ... invalid config
대응:
ConfigMap/Secret키 이름 오타 확인valueFrom.secretKeyRef.key존재 여부 확인- 앱이 문자열을 기대하는데 숫자/불리언으로 파싱 실패하는 케이스 점검
3-2. ConfigMap/Secret 볼륨 마운트 경로 문제
앱이 특정 경로의 설정 파일을 읽는데, 볼륨이 다른 경로로 마운트되었거나 파일명이 달라서 즉시 종료합니다.
체크:
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Mounts:/,/Conditions:/p'
디버그 컨테이너로 붙잡아둔 뒤 확인:
kubectl exec -n <namespace> -it <pod-name> -c <container-name> -- /bin/sh
ls -al <config-path>
cat <config-path>/<file>
3-3. 외부 의존성(DB, Kafka, Redis) 연결 실패로 즉시 종료
앱이 "부팅 시점에 의존성 연결이 반드시 성공해야 한다"고 가정하면, 네트워크/인증/방화벽 이슈가 CrashLoopBackOff로 보입니다.
체크:
- 로그에서
connection refused,timeout,authentication failed확인 - 동일 네임스페이스에서 DNS 확인
kubectl exec -n <namespace> -it <pod-name> -- /bin/sh -c "nslookup <service-name>"
대응 포인트:
- 부팅 시 즉시 종료 대신 재시도(backoff) 설계
- readiness에서만 의존성 체크하고, liveness는 프로세스 생존만 보는 구조로 분리
DB 연결 고갈이 원인이라면 앱은 종종 기동 직후부터 실패합니다. Spring 계열이라면 Spring Boot HikariCP 커넥션 고갈 원인 8가지도 함께 점검하면 진단 속도가 빨라집니다.
3-4. 마이그레이션/스키마 작업 실패
init 단계에서 Flyway/Liquibase가 실패하면 컨테이너가 종료될 수 있습니다.
체크:
- 로그에
Migration failed,checksum mismatch,permission denied등 - DB 계정 권한, 락 대기, DDL 실패
대응:
- 마이그레이션은
initContainer로 분리하고 실패 시 이벤트를 명확히 남기기 - 프로덕션에서 자동 DDL을 끄고 배포 파이프라인에서 수행
4) 프로브 실패: "살아있는데 죽인다" 케이스
CrashLoopBackOff의 상당수는 앱이 실제로는 살아있거나 곧 살아날 수 있는데, 잘못된 livenessProbe가 컨테이너를 계속 죽이는 경우입니다.
4-1. livenessProbe 경로/포트 불일치
체크:
kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.spec.containers[0].livenessProbe}'
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Liveness:/,/Readiness:/p'
이벤트에서 이런 메시지가 보이면 프로브가 원인일 가능성이 큽니다.
Liveness probe failed: HTTP probe failed with statuscode: 404dial tcp ... connect: connection refused
대응:
- 앱이 실제로 리스닝하는 포트와 프로브 포트 일치
/healthz같은 엔드포인트가 인증 없이 접근 가능한지 확인
4-2. startupProbe 없이 느린 기동을 liveness가 죽이는 문제
JVM 워밍업, 캐시 로딩, 마이그레이션 등으로 30~90초 이상 기동이 걸리는데 livenessProbe가 너무 빨리 시작되면 무한 재시작이 됩니다.
권장 패턴:
startupProbe로 기동 완료까지 보호readinessProbe로 트래픽 유입 제어livenessProbe는 진짜 "hung"만 잡도록 보수적으로
예시(YAML):
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 6
4-3. readiness 실패가 CrashLoopBackOff로 보이는가?
원칙적으로 readiness 실패는 재시작을 유발하지 않습니다. 하지만 운영에서 종종 다음이 섞여 오해합니다.
- readiness 실패로 트래픽이 안 들어옴
- 동시에 앱 내부 watchdog가 종료하거나, sidecar가 종료를 유발
- 혹은 liveness도 함께 실패
따라서 Events에서 Unhealthy가 어떤 프로브인지 반드시 구분하세요.
5) 리소스 문제: OOMKilled, CPU 부족, 파일 디스크
5-1. OOMKilled(Exit Code 137)
가장 명확한 신호입니다.
체크:
describe에서Last State: Terminated/Reason: OOMKilled- 종료 코드가
137인 경우가 많음
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Last State:/,/Ready:/p'
대응:
resources.limits.memory상향 또는 누수 해결- JVM이면
-Xmx가 limit보다 크지 않게 조정(컨테이너 메모리 인식 옵션 포함) - 메모리 피크가 init 단계에서 발생한다면 startupProbe로 보호하면서 튜닝
5-2. CPU limit이 너무 낮아 타임아웃과 프로브 실패 유발
CPU가 부족하면 앱은 죽지 않더라도 응답이 느려져 프로브가 실패하고 결국 재시작 루프가 됩니다.
체크:
kubectl top pod -n <namespace> <pod-name>
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Limits:/,/Requests:/p'
대응:
requests/limits를 현실적으로 조정- 프로브의
timeoutSeconds를 너무 짧게 두지 않기
5-3. ephemeral-storage 부족 또는 로그 폭증
노드 디스크 압박은 예고 없이 컨테이너 종료, Eviction으로 이어질 수 있습니다.
체크:
kubectl describe node <node-name> | sed -n '/DiskPressure/,+20p'
kubectl describe pod -n <namespace> <pod-name> | sed -n '/Evicted/,+20p'
대응:
- 로그를 stdout로만 내고 로테이션은 런타임/에이전트에 맡기기
emptyDir사용량 관리,ephemeral-storagerequest/limit 설정
6) 권한/보안 컨텍스트 문제: "Permission denied"로 즉시 종료
특히 runAsNonRoot, readOnlyRootFilesystem를 켠 뒤 많이 발생합니다.
체크:
- 로그에
EACCES,Permission denied,cannot create ... - 마운트 경로가 쓰기 가능한지
예시 대응(YAML):
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
앱이 /tmp, /var/log, /app/data 등에 쓰기를 요구하면 명시적으로 writable 볼륨을 제공해야 합니다.
7) 엔트리포인트/커맨드 문제: 잘못된 CMD, 셸 스크립트, 신호 처리
7-1. 컨테이너 커맨드 오버라이드 실수
Deployment에서 command/args를 오버라이드하다가 실제 실행 파일 경로가 틀어져 즉시 종료합니다.
체크:
kubectl get deploy -n <namespace> <deploy-name> -o jsonpath='{.spec.template.spec.containers[0].command} {.spec.template.spec.containers[0].args}'
로그에 exec: ... not found가 보이면 거의 확정입니다.
7-2. PID 1 신호 처리 문제
앱이 PID 1로 실행될 때 SIGTERM 처리/좀비 프로세스 정리가 제대로 안 되면 종료가 꼬이고 재시작이 반복되는 경우가 있습니다.
대응:
- tini 같은 init 사용(
--init또는 이미지에 포함) - graceful shutdown 시간을
terminationGracePeriodSeconds로 확보
8) 의외로 흔한 케이스: 정상 종료인데 RestartPolicy 때문에 루프
배치성 작업을 Deployment로 띄워놓고 메인 프로세스가 정상 종료(Exit Code 0, Reason: Completed)하면, Kubernetes는 다시 띄우고 또 종료되어 CrashLoopBackOff처럼 보일 수 있습니다.
체크:
Last State: Terminated/Reason: Completed- 로그가 정상 완료 메시지로 끝남
대응:
- 배치라면
Job/CronJob로 전환 - 서비스라면 프로세스가 포그라운드에서 계속 살아있게 구성
9) 네트워크/DNS/서비스디스커버리: 간헐적 루프의 주범
앱이 부팅 시점에 DNS 실패를 fatal로 처리하면 CrashLoopBackOff가 됩니다.
체크:
kubectl exec -n <namespace> -it <pod-name> -- /bin/sh -c "cat /etc/resolv.conf"
kubectl get svc -n <namespace>
kubectl get endpoints -n <namespace> <service-name>
대응:
- CoreDNS 장애/스케일 이슈 점검
- 의존성 연결 실패를 fatal로 두지 말고 재시도로 흡수
10) 현장용 "원인별" 체크리스트 요약
아래는 describe와 --previous logs에서 얻은 단서를 기준으로 바로 매칭하는 표입니다.
10-1. Exit Code 기반
137또는Reason: OOMKilled- 메모리 limit 상향
- 앱 메모리 누수/캐시 상한
- JVM
-Xmx재조정
139- 네이티브 세그폴트 가능성(이미지/라이브러리)
- 최근 베이스 이미지 변경 여부
0+Reason: Completed- 워크로드 타입이 Job이어야 하는지 검토
1또는Reason: Error- 로그 기반으로 설정/권한/의존성 실패 분기
10-2. Event 메시지 기반
Liveness probe failed- 경로/포트/timeout 확인
- startupProbe 도입
Back-off restarting failed container- 근본 원인은
--previous로그에 있음
- 근본 원인은
FailedMount,MountVolume.SetUp failed- PVC/ConfigMap/Secret 존재, 권한, 스토리지클래스
11) 재발 방지: CrashLoopBackOff를 "장애"로 키우지 않는 설계
부팅 실패를 fatal로 두지 말고 재시도 정책을 애플리케이션에 둔다
- DB/Kafka 같은 외부 의존성은 일시 장애가 정상입니다.
프로브 역할 분리
- startup: 기동 보호
- readiness: 트래픽 제어
- liveness: 진짜 hung만 감지
리소스 가시화
requests는 실제 평균,limits는 피크 기반- OOM이 났다면 limit만 올리지 말고 피크 원인을 같이 제거
의존성 장애가 연쇄로 번질 때는 트랜잭션 경계도 점검
- 예를 들어 메시지 기반 MSA에서 장애 시 중복 처리/재처리 폭주가 생기면 부하가 커져 기동 실패로 이어질 수 있습니다. 이 경우 Kafka MSA 중복처리 막는 Outbox 패턴 구현 같은 패턴도 함께 고려할 만합니다.
12) 결론: CrashLoopBackOff는 "원인"이 아니라 "결과"다
CrashLoopBackOff를 보면 먼저 describe의 Last State와 Events, 그리고 kubectl logs --previous로 종료 이유를 분류하세요. 그 다음에야 프로브/리소스/권한/의존성/워크로드 타입 중 어디를 고칠지 정확해집니다.
운영 체크리스트를 팀 런북에 그대로 옮길 수 있도록, 위 명령 3종(get, describe, logs --previous)을 기본 루틴으로 고정해두면 평균 복구 시간이 확실히 줄어듭니다.