Published on

Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅

Authors

서버가 죽었다가 살아나는 현상은 늘 비슷해 보이지만, Kubernetes의 CrashLoopBackOff는 특히 “증상은 하나, 원인은 여러 개”라서 진단 순서가 중요합니다. 무작정 이미지 재빌드나 노드 교체부터 시작하면 시간만 날리고, 같은 장애가 다시 반복됩니다.

이 글에서는 로그/이벤트 → Probe → 리소스(OOM/CPU) → 설정/의존성 순으로, 가장 흔한 CrashLoopBackOff 원인을 빠르게 좁혀가는 실전 체크리스트를 제공합니다. (EKS에서 DNS 계열 CrashLoopBackOff가 의심된다면 AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결도 함께 확인하는 것을 권장합니다.)

CrashLoopBackOff를 정확히 이해하기

  • CrashLoopBackOff컨테이너가 종료(exit) → kubelet이 재시작 → 또 종료가 반복될 때 붙는 상태입니다.
  • “왜 종료했는지”는 컨테이너 종료 코드, 이벤트(Events), 이전 로그(--previous), Probe 실패 기록에서 드러납니다.

핵심은 아래 3가지를 가장 먼저 확보하는 것입니다.

  1. 마지막 종료 사유(Exit Code / Reason)
  2. 직전 컨테이너 로그
  3. kubelet 이벤트(Probe 실패, OOMKilled, ImagePull, Mount 등)

1) 3분 안에 원인 범주를 좁히는 최소 명령 세트

아래 명령만으로도 대부분 “앱 크래시”, “Probe로 인한 재시작”, “OOMKilled”, “권한/마운트/시크릿 문제” 중 어디인지 갈립니다.

# 1) CrashLoopBackOff인 Pod 확인
kubectl get pod -n <ns> -o wide

# 2) Pod 이벤트/상태/종료코드 확인 (가장 중요)
kubectl describe pod -n <ns> <pod>

# 3) 직전(이전) 컨테이너 로그 확인
kubectl logs -n <ns> <pod> -c <container> --previous

# 4) 현재 컨테이너 로그(재시작 후 살아있다면)
kubectl logs -n <ns> <pod> -c <container> --tail=200

describe에서 특히 아래 필드를 눈여겨보세요.

  • Last State: TerminatedReason, Exit Code
  • State: WaitingReason: CrashLoopBackOff
  • Events:Back-off restarting failed container, OOMKilled, Liveness probe failed

종료 코드 빠른 해석

  • Exit Code 0: 정상 종료인데 재시작 정책 때문에 반복(예: Job을 Deployment로 띄움)
  • Exit Code 1/2: 앱 예외/설정 오류가 흔함
  • Exit Code 137: OOMKilled(메모리 부족) 가능성이 매우 큼(SIGKILL)
  • Exit Code 139: Segfault(네이티브 라이브러리/CGO/메모리 접근)

2) 로그로 잡히는 “앱 자체 크래시” 패턴

가장 흔한 케이스는 애플리케이션이 시작 직후 설정/의존성 문제로 죽는 것입니다.

(1) ConfigMap/Secret 누락 또는 환경변수 오류

kubectl logs --previous에서 다음이 보이면 거의 확정입니다.

  • KeyError, Missing env, failed to load config, permission denied(파일)
  • DB/Redis 접속 실패 후 즉시 종료(재시도 없이 종료)

대응

  • 환경변수/마운트 경로가 맞는지 확인
  • Secret 키 이름 오타, namespace 불일치 확인
  • 앱이 의존 서비스 연결 실패 시 즉시 종료하지 말고 재시도/지연을 두는 것이 운영적으로 안전합니다.
# Pod에 실제로 주입된 env 확인
kubectl exec -n <ns> <pod> -c <container> -- printenv | sort

# 마운트된 파일 확인
kubectl exec -n <ns> <pod> -c <container> -- ls -al /etc/config

(2) 컨테이너 커맨드/엔트리포인트 오타

exec: "...": executable file not found in $PATH 같은 메시지는 이미지 내부에 실행 파일이 없거나, command/args가 잘못된 경우입니다.

# 잘못된 예: 바이너리 경로 오타
containers:
  - name: app
    image: myapp:1.0
    command: ["/app/bin/starttt"]

대응: 로컬/CI에서 이미지 내부 파일을 확인하거나, 임시로 쉘 진입이 가능한 이미지로 검증합니다.

# 이미지에 쉘이 있다면
kubectl exec -n <ns> -it <pod> -c <container> -- sh
# 없으면 디버그용 ephemeral container를 고려

(3) 파이썬/노드 의존성 꼬임(런타임에서 ModuleNotFoundError)

빌드는 성공했는데 실행 시 ModuleNotFoundError가 뜨면, 가상환경/인터프리터 경로/site-packages가 어긋난 경우가 많습니다. 컨테이너에서도 동일하게 발생합니다.

컨테이너 내부에서 아래를 확인하세요.

kubectl exec -n <ns> <pod> -c <container> -- python -c "import sys; print(sys.executable); print(sys.path)"
kubectl exec -n <ns> <pod> -c <container> -- python -c "import pkgutil; print('uvicorn' in [m.name for m in pkgutil.iter_modules()])"

3) Probe(liveness/readiness/startup) 때문에 죽는 경우

앱은 살아있는데 liveness probe 실패로 kubelet이 컨테이너를 죽여서 CrashLoopBackOff가 발생할 수 있습니다. 이때 로그에는 “정상 시작”이 찍히는데도 재시작합니다.

(1) 이벤트에서 Probe 실패를 먼저 확인

kubectl describe pod -n <ns> <pod>
# Events:
#  Liveness probe failed: HTTP probe failed with statuscode: 500
#  Killing container with id ...

(2) startupProbe가 필요한데 liveness만 있는 패턴

초기화가 긴 앱(마이그레이션, 캐시 워밍업, 모델 로딩 등)은 시작 중에 liveness가 먼저 때리면 죽습니다. 이때는 startupProbe로 “부팅 완료 전에는 죽이지 말라”를 명시해야 합니다.

containers:
  - name: app
    image: myapp:1.0
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /healthz
        port: 8080
      failureThreshold: 60   # 60 * periodSeconds 동안 기동 유예
      periodSeconds: 2
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 0
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2

포인트

  • startupProbe가 성공한 이후에만 livenessProbe가 의미를 갖습니다.
  • readiness는 “트래픽 받을 준비”이고, liveness는 “죽었는지”입니다. readiness 실패는 재시작 사유가 아닙니다.

(3) HTTP 500/timeout의 진짜 원인: 의존성/DNS/네트워크

Probe는 단지 “헬스 체크가 실패했다”만 알려줍니다. 실패 이유는 보통 아래입니다.

  • 앱 내부에서 DB/캐시 연결이 안 되면 /healthz가 500을 반환
  • DNS 타임아웃으로 외부 의존성 접근이 지연되어 Probe timeout

EKS에서 DNS 계열이면 CoreDNS 리소스/노드 네트워크/엔드포인트 문제로 번질 수 있어, 별도 가이드(AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결)를 참고해 빠르게 분리 진단하는 것이 좋습니다.

4) 리소스(OOMKilled/CPU Throttling)로 인한 CrashLoopBackOff

(1) OOMKilled: Exit Code 137 + 이벤트 확인

describe에서 아래가 보이면 거의 확정입니다.

  • Last State: TerminatedReason: OOMKilled
  • 혹은 Exit Code: 137
kubectl describe pod -n <ns> <pod> | sed -n '/Last State:/,/Events:/p'

대응 전략

  • resources.limits.memory를 올리거나, 메모리 사용량을 줄입니다.
  • 특히 JVM/Node/Python에서도 “초기 로딩 때 피크”가 발생할 수 있어, 평상시 metrics만 보고 limits를 잡으면 터집니다.
resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "512Mi"

메모리 피크를 보려면 metrics-server/Prometheus가 필요하지만, 최소한 현재 시점은 이렇게 확인합니다.

kubectl top pod -n <ns>
kubectl top node

(2) CPU Throttling으로 “헬스 체크 타임아웃 → 재시작”

CPU limit이 너무 낮으면 애플리케이션이 느려지고, 결과적으로 probe timeout이 발생해 재시작 루프로 들어갈 수 있습니다. 이 경우 이벤트에는 Liveness probe failed: context deadline exceeded가 찍히지만, 근본 원인은 CPU입니다.

대응

  • timeoutSeconds를 무작정 늘리기 전에 CPU limit/request를 현실화
  • 초기화 구간에는 startupProbe로 보호

5) 볼륨/권한/파일시스템 문제로 즉시 종료하는 경우

다음 이벤트가 보이면 앱 문제가 아니라 쿠버네티스/노드/스토리지 계층입니다.

  • MountVolume.SetUp failed
  • permission denied (특히 non-root + PV)
  • Read-only file system
kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'

(1) securityContext로 인한 권한 문제

non-root로 실행하면서 PV 디렉터리 권한이 맞지 않으면, 앱이 로그/캐시 파일 쓰기에서 죽습니다.

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  runAsGroup: 10001
  fsGroup: 10001
  • fsGroup는 볼륨 파일 권한을 맞추는 데 자주 필요합니다.

6) “정상 종료”인데 CrashLoopBackOff처럼 보이는 배치/엔트리 설계 문제

컨테이너가 일을 끝내고 exit 0으로 종료하는 것이 정상인 워크로드를 Deployment로 띄우면, Kubernetes는 계속 재시작합니다.

체크 포인트

  • Exit Code 0인데 재시작이 반복된다면
    • Deployment가 아니라 Job/CronJob이 맞는지
    • restartPolicy가 의도와 맞는지
apiVersion: batch/v1
kind: Job
metadata:
  name: one-shot-task
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: task
          image: mytask:1.0
          command: ["python", "run_once.py"]

7) 디버깅을 더 빠르게 만드는 실전 팁 5가지

(1) 재시작이 너무 빨라 로그를 못 볼 때: sleep로 붙잡기

임시로 entrypoint를 바꿔 컨테이너를 살려두고 내부를 조사합니다.

containers:
  - name: app
    image: myapp:1.0
    command: ["sh", "-c", "sleep 3600"]

원인 파악 후 반드시 원복하세요.

(2) --previous는 습관처럼

CrashLoopBackOff에서는 “현재 로그”가 아니라 “직전 죽은 컨테이너 로그”가 핵심입니다.

kubectl logs -n <ns> <pod> -c <container> --previous --tail=300

(3) 이벤트 정렬로 원인 흐름 보기

kubectl get events -n <ns> --sort-by='.lastTimestamp'

Probe 실패 → Killing → Back-off 순서가 명확해집니다.

(4) readiness는 트래픽 차단용으로만 쓰기

readiness 실패를 “재시작 트리거”로 쓰면 장애가 증폭됩니다. 재시작은 liveness/startup에 맡기고, readiness는 의존성 준비/트래픽 보호에 집중하세요.

(5) 외부 헬스체크(ALB/NLB)와 Probe의 상호작용 점검

Ingress/ALB 헬스체크가 빡세면, readiness가 흔들릴 때 502/504로 관측될 수 있습니다. L7 헬스체크 튜닝은 AWS ALB 502·504 난사 - 원인별 해결 체크리스트도 참고하면 좋습니다.

8) 원인별 요약 테이블(현장에서 바로 쓰는 체크리스트)

증상/단서describe / 이벤트로그 패턴1차 조치
앱 시작 직후 즉시 종료Exit Code 1/2설정/의존성 에러logs --previous, env/secret/command 점검
OOMKilledReason: OOMKilled / Exit 137메모리 부족 전조/없을 수도memory limit 상향, 메모리 누수/피크 분석
Probe로 킬Liveness probe failedKilling앱은 살아보이기도 함startupProbe 도입, timeout/period 조정, 헬스 엔드포인트 분리
마운트 실패MountVolume.SetUp failed앱 로그 거의 없음PV/Secret/ConfigMap, 권한(securityContext) 점검
정상 종료 반복Exit 0작업 완료 로그Deployment → Job/CronJob 전환
DNS/네트워크 이슈타임아웃/연결 실패 이벤트i/o timeout, NXDOMAINCoreDNS/네트워크 분리 진단

마무리: “CrashLoopBackOff”를 상태가 아니라 흐름으로 보자

CrashLoopBackOff는 하나의 에러가 아니라 재시작 루프라는 결과입니다. 따라서 진단은 항상 “종료 코드/이벤트/직전 로그”로 시작해, Probe와 리소스로 확장하는 순서가 가장 빠릅니다.

  • describe podReason/Exit Code/Events를 먼저 확정
  • logs --previous죽기 직전의 단서를 확보
  • Probe 설정은 startupProbe초기화 구간을 보호
  • OOM/CPU는 limit이 아니라 실제 피크 기준으로 재설계

이 순서대로만 해도, 대부분의 CrashLoopBackOff는 10~20분 내에 “원인 범주”가 결정되고, 재발 방지까지 연결할 수 있습니다.