Published on

K8s CrashLoopBackOff 원인별 10분 진단법

Authors

서버가 죽고 다시 뜨고, 또 죽는 루프는 운영자에게 가장 불쾌한 신호입니다. Kubernetes에서 CrashLoopBackOff컨테이너가 반복적으로 비정상 종료하고 kubelet이 재시작을 시도하되, 재시작 간격을 점점 늘리는(backoff) 상태를 의미합니다. 중요한 점은 CrashLoopBackOff 자체가 원인이 아니라 증상이라는 것입니다.

이 글은 “10분 안에 원인 범위를 좁히는” 것을 목표로 합니다. 핵심은 딱 4가지 데이터를 빠르게 확보하는 것입니다.

  • 이벤트(Event): 무엇 때문에 재시작이 트리거됐는가
  • 종료코드(Exit Code) / 종료사유(Reason): 프로세스가 왜 죽었는가
  • 로그(Log): 앱이 죽기 직전에 무슨 말을 했는가
  • 리소스/프로브/설정: 죽게 만든 환경적 조건은 무엇인가

> 참고: OOM이 의심된다면 리눅스 관점의 원인/방지도 함께 보면 좋습니다. 리눅스 OOM Killer로 프로세스 죽음 진단·방지

0~2분: “가장 먼저” 실행할 5개 명령

아래 명령만으로도 절반 이상은 방향이 잡힙니다.

# 1) 상태/재시작 횟수/노드 확인
kubectl -n <ns> get pod <pod> -o wide

# 2) 이벤트: BackOff, OOMKilled, FailedMount, Unhealthy 등 키워드가 바로 나옴
kubectl -n <ns> describe pod <pod>

# 3) 직전 크래시 로그(가장 중요)
kubectl -n <ns> logs <pod> --previous --all-containers

# 4) 현재 로그(죽기 직전/직후 비교)
kubectl -n <ns> logs <pod> --all-containers --tail=200

# 5) 종료코드/Reason을 JSONPath로 빠르게
kubectl -n <ns> get pod <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\t"}{.restartCount}{"\n"}{end}'

10분 진단의 분기 기준(치트시트)

  • reason=OOMKilled 또는 exitCode 137메모리 부족(OOM)
  • exitCode 1/2 + 앱 로그에 stacktrace → 앱 설정/의존성/마이그레이션 문제
  • FailedMount, MountVolume.SetUp failed볼륨/시크릿/권한/CSI
  • Unhealthy 이벤트 + Readiness/Liveness probe failed프로브 설정/부팅 지연/엔드포인트
  • CreateContainerConfigError, ImagePullBackOff이미지/환경변수/시크릿/레지스트리
  • 노드 이벤트에 Evicted/DiskPressure노드 자원 압박

2~10분: 원인별 “즉시” 진단 루틴

아래는 CrashLoopBackOff의 대표 원인들을 증상 → 확인 포인트 → 1차 조치 순으로 정리한 실전 루틴입니다.

1) OOMKilled (메모리 부족) — exitCode 137

증상

  • describeOOMKilled 또는 Container killed by OOM 문구
  • 종료코드 137(= SIGKILL)
  • 재시작이 일정 주기로 반복(트래픽/배치 타이밍과 연동되기도)

1분 확인

kubectl -n <ns> describe pod <pod> | sed -n '/State:/,/Conditions:/p'
kubectl -n <ns> top pod <pod>
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[*].resources}'

1차 조치(가장 빠른 순)

  1. 메모리 limit 상향 또는 limit 제거(권장 X, 임시)
  2. JVM/Node/Python 등 런타임 힙 제한을 limit에 맞추기
  3. 메모리 누수 의심 시, 재현 트래픽/배치 구간에서 프로파일링

예: Deployment 메모리 상향 패치

kubectl -n <ns> patch deploy <deploy> --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"1024Mi"},
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"512Mi"}
]'

> OOM이 반복되면 “앱이 진짜로 메모리를 더 필요로 하는지”와 “limit에 맞춘 런타임 설정인지”를 분리해서 봐야 합니다. 자세한 원인 분석은 리눅스 OOM Killer로 프로세스 죽음 진단·방지도 같이 참고하세요.

2) Liveness/Readiness Probe 실패로 강제 재시작

증상

  • 이벤트에 Unhealthy, Liveness probe failed 반복
  • 앱 자체는 살아있는데 kubelet이 죽였다가 재시작
  • 부팅이 느린 서비스(마이그레이션/캐시 워밍업)에서 흔함

2분 확인

kubectl -n <ns> describe pod <pod> | sed -n '/Liveness:/,/Events:/p'

# 프로브 엔드포인트를 포트포워딩으로 직접 확인
kubectl -n <ns> port-forward pod/<pod> 18080:8080
curl -i http://127.0.0.1:18080/healthz

1차 조치

  • startupProbe 도입(부팅 구간 보호)
  • liveness의 initialDelaySeconds, timeoutSeconds, failureThreshold 조정
  • readiness는 트래픽 차단용, liveness는 “진짜 죽었을 때만”

예: startupProbe 추가 예시

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  timeoutSeconds: 2
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  timeoutSeconds: 2
  periodSeconds: 5
  failureThreshold: 3
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2

3) 설정/환경변수/시크릿 오류 (exitCode 1, CreateContainerConfigError)

증상

  • 앱 로그에 “필수 env 없음”, “config parse error”, “cannot read secret”
  • 또는 컨테이너가 뜨기 전 CreateContainerConfigError

2분 확인

kubectl -n <ns> describe pod <pod> | sed -n '/Environment:/,/Mounts:/p'

# secret/configmap 존재 여부
kubectl -n <ns> get secret,cm | grep -E '<secret-name>|<cm-name>'

# 키 누락 확인
kubectl -n <ns> get secret <secret-name> -o jsonpath='{.data}'

1차 조치

  • Secret/ConfigMap 키 이름 오타, base64 인코딩 여부 확인
  • envFrom 사용 시 예상치 못한 키 충돌 점검
  • 애플리케이션이 “필수 값 누락 시 즉시 종료”하도록 되어 있으면 CrashLoop가 빠르게 발생

4) 이미지/엔트리포인트/권한 문제 (exec format error, permission denied)

증상

  • 로그에 exec format error (아키텍처 불일치: arm64/amd64)
  • permission denied (실행 비트/USER 권한)
  • no such file or directory (ENTRYPOINT 경로)

2분 확인

kubectl -n <ns> describe pod <pod> | sed -n '/Image:/,/Environment:/p'

# 노드 아키텍처 확인
kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.architecture}{"\n"}{end}'

1차 조치

  • 멀티아치 이미지(manifest list)로 빌드/푸시
  • Dockerfile에서 ENTRYPOINT/CMD 경로, 실행권한(chmod +x) 확인
  • runAsNonRoot 사용 시 바이너리/디렉터리 권한 재점검

5) 볼륨 마운트 실패 (FailedMount, permission)

증상

  • 이벤트에 FailedMount, MountVolume.SetUp failed
  • CSI/IAM 권한 부족, PVC Pending, Secret volume 누락 등

2분 확인

kubectl -n <ns> describe pod <pod> | sed -n '/Volumes:/,/QoS Class:/p'
kubectl -n <ns> get pvc
kubectl -n <ns> describe pvc <pvc>

1차 조치

  • PVC가 Bound인지, StorageClass/CSI 드라이버 정상인지 확인
  • Secret/ConfigMap volume이면 리소스 존재/키 확인
  • EKS라면 IRSA/IAM 권한 문제로도 파생될 수 있음(스토리지/컨트롤러)

6) 의존 서비스 불가로 앱이 즉시 종료 (DB/Redis/Kafka/DNS)

증상

  • 앱 로그: ECONNREFUSED, connection timeout, getaddrinfo ENOTFOUND
  • readiness 실패가 아니라 “앱이 부팅 중 연결 실패 시 종료”하는 패턴

3분 확인

  1. 같은 네임스페이스에서 임시 디버그 파드로 네트워크 확인
kubectl -n <ns> run net-debug --rm -it --image=busybox:1.36 --restart=Never -- sh

# DNS
nslookup <service-name>

# TCP 연결
wget -S -O- http://<service-name>:<port> 2>&1 | head
  1. 서비스/엔드포인트 확인
kubectl -n <ns> get svc <service>
kubectl -n <ns> get endpoints <service>

1차 조치

  • 앱을 “의존 서비스 준비 전에는 재시도(backoff)하며 살아있게” 수정(권장)
  • initContainer로 의존성 체크 후 본 컨테이너 시작
  • 클러스터 DNS/네트워크 정책(NetworkPolicy) 차단 여부 확인

> EKS에서 네트워크/엔드포인트 계열 이슈는 502/504, TLS 실패로도 이어집니다. 인그레스/ALB까지 연쇄 장애가 의심되면 EKS ALB Ingress 502/504 - TLS 핸드셰이크 실패 진단도 같이 보면 원인 범위를 더 빨리 줄일 수 있습니다.

7) 애플리케이션 마이그레이션/배치 작업이 매번 실행되어 실패

증상

  • 컨테이너 시작 시 DB migration 수행 → 실패 → 종료 → 재시작 시 또 migration
  • 로그에 migration tool 출력이 반복

2분 확인

kubectl -n <ns> logs <pod> --previous --all-containers | tail -n 200

1차 조치

  • migration을 Job/InitContainer로 분리(한 번만 실행)
  • 실패 시 종료 대신 재시도/알람 후 대기하는 전략 고려

8) 노드 자원 압박/퇴거(Eviction), 디스크 부족

증상

  • Evicted, DiskPressure, ImageGCFailed 등 노드 이벤트
  • 특정 노드에만 스케줄링되면 반복 발생

2분 확인

kubectl describe pod <pod> -n <ns> | grep -E 'Node:|Evicted|DiskPressure|Reason'
kubectl describe node <node> | sed -n '/Conditions:/,/Addresses:/p'

1차 조치

  • 노드 디스크/인오드 정리, 로그 폭증 확인
  • requests/limits를 현실화(QoS 개선)
  • 문제 노드 cordon/drain 후 교체

9) 종료 시그널 처리 실패(Graceful shutdown 미구현)로 재기동 루프 유발

증상

  • 배포/스케일링/노드 드레인 시 SIGTERM 처리 못하고 즉시 종료
  • preStop 훅/terminationGracePeriodSeconds 부족

확인 및 조치

terminationGracePeriodSeconds: 60
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]
  • 앱에서 SIGTERM 핸들러 구현(서버 close, 큐 flush)

10) “진단 속도”를 올리는 디버깅 패턴 3가지

A. --previous 로그를 습관화

CrashLoop에서는 현재 로그보다 직전 종료 로그가 결정적입니다.

kubectl -n <ns> logs <pod> --previous

B. Ephemeral Container로 살아있는 순간에 내부 확인

컨테이너가 너무 빨리 죽으면, ephemeral container로 내부 파일/환경을 확인합니다.

kubectl -n <ns> debug -it pod/<pod> --image=busybox:1.36 --target=<container-name>

C. 재시작 지연을 “의도적으로” 늘려 관찰

앱이 너무 빨리 죽어 관찰이 어려우면, 임시로 커맨드를 바꿔 쉘 대기 상태로 올려 원인을 확인합니다(운영 환경에서는 주의).

kubectl -n <ns> patch deploy <deploy> --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"]}
]'

마무리: 10분 내 결론을 내는 순서

CrashLoopBackOff를 빠르게 끝내려면 “추측”보다 “정렬된 증거”가 필요합니다.

  1. describe pod 이벤트에서 1차 분류
  2. logs --previous로 직전 크래시 메시지 확보
  3. 종료코드/Reason으로 OOM/시그널/에러 구분
  4. 프로브/리소스/볼륨/시크릿/의존성 순서로 체크
  5. 임시 조치(리소스 상향, startupProbe, 설정 수정) 후 재발 방지(구조 개선)

이 루틴을 팀의 런북으로 고정해두면, CrashLoopBackOff는 “장애”가 아니라 “디버깅 가능한 이벤트”로 바뀝니다.