Published on

K8s CrashLoopBackOff 원인 10분 진단법

Authors

CrashLoopBackOff는 에러 자체라기보다 쿠버네티스가 컨테이너를 재시작했는데 또 바로 죽어서 백오프(backoff)로 재시작 간격을 늘리는 상태입니다. 즉, 핵심은 “왜 프로세스가 종료됐는가”를 빠르게 특정하는 것입니다.

운영에서 중요한 건 완벽한 포스트모템보다 시간 제한을 둔 진단 루틴입니다. 아래는 제가 장애 대응 때 쓰는 “10분 플로우”입니다. 대부분의 CrashLoopBackOff는 이 루틴으로 원인을 좁힐 수 있습니다.

0분~2분: 상태를 ‘정확히’ 본다 (Pod, 컨테이너, 종료 사유)

먼저 감으로 로그부터 보지 말고, 상태 필드에서 종료 코드와 메시지를 확인합니다.

# 1) Pod 이벤트와 컨테이너 상태(Last State 포함)
kubectl -n <namespace> describe pod <pod-name>

# 2) 컨테이너별 재시작 횟수/상태를 빠르게 확인
kubectl -n <namespace> get pod <pod-name> -o wide

# 3) JSONPath로 종료 코드/사유만 뽑기
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason} {.status.containerStatuses[*].lastState.terminated.exitCode} {.status.containerStatuses[*].lastState.terminated.message}'

여기서 가장 먼저 봐야 할 포인트:

  • lastState.terminated.exitCode
    • 0 이면 “정상 종료”인데, Deployment/Job 설계가 잘못됐을 수 있습니다.
    • 137 이면 OOMKill 가능성이 큽니다.
    • 139 는 세그폴트(네이티브 크래시) 가능성이 있습니다.
  • reasonError, OOMKilled, Completed 중 무엇인지
  • Events 섹션의 Back-off restarting failed container 직전 이벤트

2분~4분: “직전 로그”를 본다 (현재 로그 말고 이전 로그)

CrashLoopBackOff에서는 현재 컨테이너가 이미 죽어버려서 이전 인스턴스 로그가 결정적입니다.

# 이전 컨테이너 로그(가장 중요)
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous

# 현재 떠있는 인스턴스 로그(살아있다면)
kubectl -n <namespace> logs <pod-name> -c <container-name>

# 마지막 200줄만 빠르게
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous --tail=200

로그에서 빠르게 분류되는 패턴:

  • 설정 파일/환경변수 누락: KeyError, Missing env, Invalid config
  • 포트 바인딩 실패: Address already in use, Permission denied
  • 외부 의존성 실패: DB 연결 실패, Kafka 인증 실패, DNS 실패
  • 애플리케이션이 너무 빨리 종료: Started ... 이후 바로 Shutting down 또는 아무 로그 없이 종료

4분~6분: 프로브(liveness/readiness/startup)로 죽는지 확인

많은 CrashLoopBackOff가 “앱이 죽어서”가 아니라 liveness probe가 앱을 죽여서 발생합니다. describe pod 이벤트에 아래 같은 메시지가 있으면 프로브를 의심하세요.

  • Liveness probe failed
  • Readiness probe failed 자체는 재시작을 유발하지 않지만, 설정이 꼬여서 liveness와 함께 문제를 만들 수 있습니다.
# 프로브 설정 확인
kubectl -n <namespace> get deploy <deploy-name> -o yaml

프로브 진단 체크리스트:

  • startupProbe 없이 livenessProbe 가 너무 이르게 시작되는가
  • initialDelaySeconds 가 앱 부팅 시간보다 짧은가
  • timeoutSeconds 가 너무 짧은가 (특히 TLS 핸드셰이크/DB 마이그레이션 수행 시)
  • HTTP 프로브 경로가 인증을 요구하는 엔드포인트인가
  • gRPC 앱인데 HTTP 프로브를 잘못 쏘고 있지는 않은가

프로브 때문에 재시작되는 전형적 상황:

  • Spring Boot가 DB 마이그레이션/캐시 워밍업 때문에 40초 걸리는데 liveness가 10초부터 때림
  • /health 가 인증 필요하거나, 내부 라우팅 미구성으로 302/401/404 반환

해결은 보통 아래 중 하나입니다.

  • startupProbe 추가로 “부팅 완료 전에는 죽이지 않기”
  • liveness는 “프로세스가 살아있는지”만 보고, readiness로 트래픽 차단

6분~8분: OOMKilled/리소스/노드 이슈를 확인

Exit code 137 또는 reason: OOMKilled 가 보이면 거의 확정입니다.

# 종료 사유가 OOMKilled인지 빠르게 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'

# 리소스 요청/제한 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}'

# metrics-server가 있다면 현재 사용량 확인
kubectl -n <namespace> top pod <pod-name>

OOMKilled의 흔한 원인:

  • resources.limits.memory 가 너무 낮음
  • JVM/Node/Python 런타임이 컨테이너 메모리 제한을 고려하지 않음
  • 트래픽 급증으로 힙/버퍼가 순간적으로 튐

빠른 대응 가이드:

  • 임시로 limits.memory 상향 (원인 파악 전 “불 끄기”)
  • JVM이면 -XX:MaxRAMPercentage 또는 -Xmx 를 컨테이너 제한에 맞추기
  • Node.js면 --max-old-space-size 조정

추가로 노드 레벨의 압박도 확인합니다.

kubectl describe node <node-name>

MemoryPressure, DiskPressure 가 있으면 Pod가 축출(eviction)되거나 정상 기동이 어려울 수 있습니다.

8분~10분: “앱이 아니라 런타임/이미지/엔트리포인트” 문제를 확인

로그가 거의 없고 즉시 종료된다면, 애플리케이션 코드보다 컨테이너 실행 자체를 의심해야 합니다.

1) 엔트리포인트/커맨드 오류

  • exec format error (아키텍처 불일치: ARM 이미지로 x86 노드에서 실행 등)
  • no such file or directory (엔트리포인트 경로/권한)
# 실제 command/args가 어떻게 들어갔는지 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.containers[*].command} {.spec.containers[*].args}'

2) ConfigMap/Secret 마운트 문제

  • 파일이 있어야 하는데 비어 있음
  • 키 이름 오타
  • 권한/소유자 문제
# 볼륨/마운트 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.spec.volumes} {.spec.containers[*].volumeMounts}'

# ConfigMap/Secret 실제 데이터 확인
kubectl -n <namespace> get configmap <cm-name> -o yaml
kubectl -n <namespace> get secret <secret-name> -o yaml

3) 이미지 Pull 문제와의 구분

ImagePullBackOff 는 CrashLoopBackOff와 다릅니다. 다만 운영에서는 두 상태가 섞여 보이기도 하니, 이벤트에서 pull 관련 메시지가 보이면 아래 글도 함께 확인해두면 좋습니다.

자주 나오는 CrashLoopBackOff 원인 7가지 (패턴별 처방)

1) 앱이 “정상 종료(Exit 0)” 하는데 Deployment로 띄움

워커/배치가 할 일을 끝내고 종료하는데 Deployment로 실행하면 재시작 루프가 납니다.

  • 해결: Job/CronJob으로 전환하거나, 프로세스를 데몬 형태로 유지

2) DB 마이그레이션/의존성 실패로 부팅 중 종료

Flyway, Liquibase, 초기화 스크립트가 DB 연결 실패 시 프로세스를 종료시키는 경우가 많습니다.

  • 해결: 의존성 준비 전에는 재시도하도록 변경, 혹은 initContainer로 의존성 체크

3) livenessProbe가 너무 공격적

  • 해결: startupProbe 도입, initialDelaySeconds/failureThreshold 조정

4) OOMKilled

  • 해결: 메모리 상향, 런타임 힙 상한 설정, 누수 점검

5) 환경변수/Secret 누락

  • 해결: required 값은 앱 시작 시 명확히 로그로 남기고, 배포 파이프라인에서 검증

6) 파일 권한/실행 권한 문제

  • 해결: Dockerfile에서 chmod +x, USER 변경 시 권한 재점검, 보안 컨텍스트 확인

7) SIGTERM 처리 미흡으로 롤링 업데이트 중 반복 재시작

종료 시그널을 제대로 처리하지 못해 종료가 지연되거나, 다음 기동에서 락 파일/포트 점유가 남아 크래시로 이어질 수 있습니다.

  • 해결: graceful shutdown 구현, terminationGracePeriodSeconds 조정

10분 진단을 더 빠르게 만드는 “원샷” 커맨드 세트

아래 3개만 외워도 초동이 빨라집니다.

# 1) 이벤트/상태
kubectl -n <namespace> describe pod <pod-name>

# 2) 이전 로그
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous --tail=200

# 3) 종료 코드/사유만 핀포인트
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason} {.status.containerStatuses[*].lastState.terminated.exitCode}'

재발 방지 체크리스트 (운영 관점)

CrashLoopBackOff는 “원인”이 다양해서, 재발 방지는 관측 가능성(Observability)과 배포 안전장치가 핵심입니다.

  • 애플리케이션 시작 실패 시 명확한 에러 로그를 남기기 (필수 설정값, 연결 대상, 설정 파일 경로)
  • startupProbe 와 readiness/liveness 역할 분리
  • 리소스 requests/limits 를 “측정 기반”으로 설정하고, OOM 알림 구성
  • 배포 전 smoke test: 최소한 kubectl run 또는 프리뷰 환경에서 환경변수/Secret 검증
  • 외부 의존성 장애 시 즉시 종료 대신 지수 백오프 재시도(단, 무한 대기 시 readiness로 트래픽 차단)

부팅/헬스체크 설계가 애매한 Spring 계열 서비스라면, 장애 패턴 자체를 줄이기 위해 런타임/운영 함정도 함께 점검해두는 게 좋습니다.

마무리

CrashLoopBackOff 대응에서 중요한 건 “무엇을 먼저 볼지”입니다. describe 로 종료 사유와 이벤트를 확인하고, --previous 로그로 직전 크래시 원인을 잡고, 프로브와 OOM을 우선 배제하면 10분 내에 대부분 결론이 납니다.

그래도 원인이 불명확하다면, 다음 단계는 컨테이너에 디버그 쉘을 붙이거나(에페메럴 컨테이너), 동일 이미지로 로컬 재현을 통해 엔트리포인트/환경 차이를 좁히는 방식으로 넘어가면 됩니다.