Published on

K8s CrashLoopBackOff 원인 10분 추적법

Authors

CrashLoopBackOff는 증상이 아니라 상태입니다. 쿠버네티스가 컨테이너를 재시작하려고 시도했는데, 짧은 시간 안에 계속 종료되어 백오프(backoff)로 재시작 간격을 늘리는 상황을 말합니다. 중요한 포인트는 두 가지입니다.

  • 무엇이 컨테이너를 종료시켰는가(Exit Code, Signal, OOMKill)
  • 왜 쿠버네티스가 “살아있지 않다”고 판단했는가(프로브, 준비 상태, 의존성)

이 글은 운영 중인 클러스터에서 10분 안에 원인을 좁히는 “추적 루틴”을 제공합니다. 원인 목록을 더 넓게 보고 싶다면 함께 읽을 만한 글로 K8s CrashLoopBackOff 원인 10가지·즉시 진단법도 참고하세요.

0분~1분: 대상 Pod와 컨테이너를 확정하기

먼저 “어떤 Pod의 어떤 컨테이너”가 죽는지 확정해야 합니다. 사이드카가 죽는 경우도 흔합니다.

# 네임스페이스 포함해서 빠르게 확인
kubectl get pod -n my-ns -o wide | grep -i crash

# 특정 파드 자세히
kubectl describe pod -n my-ns my-pod

describe에서 다음을 눈으로 먼저 잡습니다.

  • Containers: 아래 State: WaitingReason: CrashLoopBackOff
  • Last State: TerminatedExit Code, Reason, Finished
  • Events: 에 반복되는 경고(프로브 실패, 이미지 풀 실패, OOM 등)

여기서 이미 절반은 끝납니다. Exit Code137이면 OOMKill 가능성이 크고, 1이면 앱이 자체적으로 실패 종료했을 확률이 큽니다.

1분~3분: “직전 로그”부터 본다 (현재 로그 말고)

CrashLoop 상황에서 가장 흔한 실수는 “현재 실행 중인 컨테이너 로그”만 보는 것입니다. 재시작 직후라 로그가 비어 있을 수 있습니다. 반드시 --previous를 먼저 봅니다.

# 직전 크래시 로그
kubectl logs -n my-ns my-pod -c my-container --previous

# 현재 실행 중 로그(살아있을 때)
kubectl logs -n my-ns my-pod -c my-container -f

로그에서 찾는 키워드 패턴은 다음과 같습니다.

  • 설정/환경: missing env, config not found, permission denied, no such file
  • 네트워크/의존성: connection refused, timeout, dns, x509, certificate
  • 마이그레이션/스키마: migration failed, relation does not exist
  • 런타임: panic, segmentation fault, SIGILL, SIGSEGV

로그가 전혀 없다면 “프로세스가 시작도 못 했거나, stdout으로 아무것도 못 찍고 죽었거나”입니다. 이때는 describeLast State와 이벤트, 그리고 command/args를 확인하는 쪽으로 이동합니다.

3분~5분: 종료 원인(Exit Code, Signal, OOMKill)로 분기

여기부터는 “분기”가 핵심입니다. kubectl describe podLast State를 기준으로 아래처럼 갈라집니다.

케이스 A: Reason: OOMKilled 또는 Exit Code: 137

대부분 메모리 상한(resources.limits.memory)에 부딪혀 커널 OOM Killer가 프로세스를 죽인 것입니다.

# 리소스 설정 확인
kubectl get pod -n my-ns my-pod -o jsonpath='{.spec.containers[*].resources}'

# 메트릭 서버가 있다면 현재 사용량 확인
kubectl top pod -n my-ns my-pod
kubectl top pod -n my-ns --containers | grep my-pod

즉시 취할 액션:

  • limits.memory를 올리거나, 메모리 누수/캐시 설정을 점검
  • JVM/Node/Go 런타임이면 힙 상한을 명시적으로 조정
  • 대용량 초기 로딩(예: 모델, 사전, 캐시)을 지연 로딩으로 변경

주의: 메모리 requests만 높이고 limits가 낮으면, 스케줄은 되지만 실행 중 죽습니다. 반대로 limits만 높고 requests가 너무 낮으면 노드 압박으로 다른 프로세스와 경쟁하다가 불안정해질 수 있습니다.

케이스 B: Exit Code: 1 (일반 오류 종료)

앱이 “정상적으로 시작하지 못했다”고 선언하고 종료한 케이스입니다. 로그가 가장 중요합니다.

바로 확인할 것:

  • 잘못된 환경변수/시크릿
  • 마이그레이션 실패
  • 외부 의존성(DB, Kafka, Redis) 연결 실패
# 환경변수/시크릿 참조 확인(값 자체를 찍지 말고 참조가 유효한지)
kubectl get deploy -n my-ns my-deploy -o yaml | sed -n '1,200p'

# 시크릿/컨피그맵 존재 여부
kubectl get secret -n my-ns | grep my-secret
kubectl get configmap -n my-ns | grep my-config

시크릿 키 이름이 바뀌었거나, envFromConfigMap이 누락되면 “시작 즉시 종료”로 이어집니다.

케이스 C: Exit Code: 139 또는 시그널 관련(세그폴트)

네이티브 라이브러리, 아키텍처 불일치, 런타임 버그, 이미지/빌드 문제 가능성이 큽니다.

# 이미지와 실행 커맨드 확인
kubectl get pod -n my-ns my-pod -o jsonpath='{.spec.containers[0].image}{"\n"}{.spec.containers[0].command}{"\n"}{.spec.containers[0].args}{"\n"}'

특히 amd64 노드에 arm64 이미지를 올리거나(반대도 포함), distroless 이미지에서 필요한 라이브러리가 빠진 경우가 많습니다.

5분~7분: 프로브(liveness/readiness/startup)로 인한 “강제 재시작” 확인

애플리케이션이 사실은 살아 있는데, 프로브 설정이 공격적이어서 쿠버네티스가 죽인 뒤 재시작시키는 경우가 있습니다. Events에 아래가 반복되면 거의 확정입니다.

  • Liveness probe failed
  • Readiness probe failed
  • Back-off restarting failed container
# 프로브 설정만 빠르게 보기
kubectl get pod -n my-ns my-pod -o jsonpath='{.spec.containers[0].livenessProbe}{"\n"}{.spec.containers[0].readinessProbe}{"\n"}{.spec.containers[0].startupProbe}{"\n"}'

자주 발생하는 함정:

  • 초기 부팅이 느린데 startupProbe 없이 livenessProbe만 둔 경우
  • initialDelaySeconds가 너무 짧고, timeoutSeconds가 너무 짧은 경우
  • 앱은 0.0.0.0이 아니라 127.0.0.1에만 바인딩하는데, 프로브는 Pod IP로 접근하는 경우
  • HTTP 프로브 경로가 리다이렉트/인증으로 바뀐 경우(예: /health가 로그인 필요)

프로브 튜닝의 기본 방향:

  • “부팅 완료 전”은 startupProbe로 보호
  • “일시적 지연”은 failureThresholdperiodSeconds로 흡수
  • “정말 죽었을 때만” livenessProbe가 재시작을 유도

7분~9분: 스케줄/볼륨/권한 문제로 시작 자체가 꼬이는지 확인

CrashLoop로 보이지만 실제로는 컨테이너가 정상 실행을 못 하고 준비 과정에서 실패하는 경우가 있습니다.

볼륨 마운트/권한

describe의 이벤트에 다음이 있으면 볼륨/권한 쪽입니다.

  • MountVolume.SetUp failed
  • permission denied
kubectl describe pod -n my-ns my-pod | sed -n '/Events:/,$p'

조치 포인트:

  • securityContext.runAsUser, fsGroup 미설정으로 파일 접근 실패
  • readOnlyRootFilesystem: true 인데 앱이 /tmp나 작업 디렉터리에 쓰려는 경우
  • PVC 바인딩/스토리지 클래스 문제

이미지 풀/레지스트리 인증

이건 보통 ImagePullBackOff지만, 재시작 과정에서 섞여 보일 때도 있습니다.

  • Failed to pull image
  • ErrImagePull

레지스트리 토큰 만료, imagePullSecrets 누락을 확인합니다.

9분~10분: 재현 가능한 “단발 실행”으로 원인 고정하기

원인을 거의 좁혔다면, 동일 이미지로 “프로브 없이” 단발 실행을 만들어 로그/행동을 고정시키는 게 빠릅니다. 운영 리소스를 직접 건드리기 어렵다면 임시 Pod로 재현합니다.

# 같은 이미지로 임시 디버그 파드 실행(명령은 상황에 맞게)
kubectl run -n my-ns crash-debug \
  --image=my-registry/my-image:tag \
  --restart=Never \
  --command -- sh -c 'env; ./app --version; ./app'

# 로그 확인
kubectl logs -n my-ns crash-debug

# 필요 시 파드 내부 진입(이미지에 sh가 있어야 함)
kubectl exec -n my-ns -it crash-debug -- sh

distroless처럼 셸이 없는 이미지라면, 별도 디버그 이미지(예: busybox, alpine)를 같은 네임스페이스에서 띄우고 네트워크/ DNS/ TLS만 확인하는 방식이 현실적입니다.

실전 체크리스트: 10분 루틴을 한 장으로 요약

아래 순서대로만 하면 “감”이 아니라 “증거”로 좁힙니다.

  1. kubectl describe podLast State, Exit Code, Events 확보
  2. kubectl logs --previous로 직전 크래시 로그 확보
  3. Exit Code 137이면 메모리/OOM 분기, Exit Code 1이면 설정/의존성 분기
  4. Events에 프로브 실패가 반복되면 프로브 튜닝/경로 확인
  5. 볼륨/권한/시크릿 누락은 Eventsspec에서 바로 드러남
  6. 임시 Pod로 단발 실행해 재현하고 원인을 고정

자주 나오는 원인별 “증거”와 즉시 처방

DB 의존성(연결 실패)로 즉시 종료

  • 증거: 로그에 connection refused, timeout, password authentication failed
  • 처방: 네트워크 정책/서비스 DNS/시크릿/커넥션 스트링 확인, 앱 시작 시 재시도(backoff) 추가

마이그레이션을 앱 시작에 묶어둔 경우

  • 증거: migration failed 후 종료
  • 처방: 마이그레이션을 Job으로 분리하거나, 실패 시 롤백/재시도 전략 수립

DB 락/교착이 의심되면 애플리케이션 크래시로만 보지 말고 데이터베이스 관점에서 확인이 필요합니다. 이 경우 PostgreSQL 데드락(40P01) 원인·해결 9단계가 원인 좁히기에 도움이 됩니다.

설정 스크립트에서 set -e로 조기 종료

엔트리포인트 셸 스크립트가 작은 실패에도 즉시 종료하면서 CrashLoop가 나는 경우가 많습니다.

  • 증거: 로그가 짧고, 특정 커맨드 실패 직후 종료
  • 처방: 안전한 예외 처리, 명시적 에러 메시지, 필수/선택 단계 분리

관련해서 셸 스크립트의 실패 처리 패턴은 bash set -euo pipefail 함정과 안전한 예외처리를 참고하면 시행착오를 줄일 수 있습니다.

결론: CrashLoopBackOff는 “원인 수렴 속도”가 전부다

CrashLoopBackOff를 해결하는 가장 빠른 방법은 복잡한 추측이 아니라, describeLast StateEvents, 그리고 --previous 로그로 증거를 모아 분기하는 것입니다. 10분 루틴으로도 대부분의 케이스는 OOM, 프로브, 설정/시크릿, 의존성 연결, 권한/볼륨 중 하나로 수렴합니다.

다음 장애에서 시간을 더 줄이려면, 평소에 아래를 준비해 두면 좋습니다.

  • 애플리케이션 시작 로그에 “환경/의존성 체크 결과”를 명확히 남기기
  • 프로브를 서비스 특성에 맞게 튜닝하고, 초기 부팅은 startupProbe로 보호
  • 리소스 requests/limits를 근거 있는 값으로 관리하고 OOM 시그널을 관측 가능하게 만들기

이 정도만 갖춰도 CrashLoopBackOff는 ‘공포의 상태’가 아니라 ‘빠르게 추적 가능한 이벤트’가 됩니다.