Published on

K8s CrashLoopBackOff 원인 10분 추적법

Authors
Binance registration banner

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는 ‘공포의 상태’가 아니라 ‘빠르게 추적 가능한 이벤트’가 됩니다.