Published on

K8s CrashLoopBackOff 원인별 로그·Probe 해결 가이드

Authors

CrashLoopBackOff는 Kubernetes가 장애를 감지해 컨테이너를 재시작하는 과정에서, 재시작 간격(back-off)이 점점 늘어나며 표시되는 상태입니다. 즉, CrashLoopBackOff 자체가 원인이 아니라 결과입니다.

이 글에서는 운영 환경에서 가장 자주 만나는 패턴을 기준으로:

  • 어떤 순서로 로그와 이벤트를 수집해야 하는지
  • livenessProbe/readinessProbe/startupProbe가 어떻게 크래시를 유발하는지
  • 원인별로 YAML을 어떻게 수정해야 하는지

를 실전 중심으로 정리합니다.

1) 먼저 확인할 것: “왜 죽었는지”를 가장 빨리 찾는 순서

CrashLoopBackOff를 보면 바로 kubectl logs부터 치는 경우가 많지만, 이벤트와 종료 코드를 먼저 보면 시간을 크게 줄일 수 있습니다.

1-1. Pod 이벤트와 종료 코드 확인

kubectl -n <namespace> describe pod <pod-name>

describe에서 특히 다음을 봅니다.

  • State: TerminatedReason / Exit Code / Finished 시간
  • Last State: Terminated (재시작 직전의 종료 정보)
  • 하단 Events 섹션

자주 나오는 힌트:

  • OOMKilled 또는 Exit Code: 137이면 메모리/리소스 문제 가능성이 큼
  • Error + Exit Code: 1이면 앱 프로세스가 스스로 종료(설정/의존성/권한 등)
  • Liveness probe failed가 반복되면 “앱은 살아있지만 Probe가 죽이고 있는” 구조일 수 있음

OOMKilled 패턴은 내용이 길어 별도 글로 정리해두었습니다.

1-2. 직전 크래시 로그 보기(--previous)

CrashLoopBackOff 상황에서 가장 중요한 옵션은 --previous입니다. 현재 컨테이너가 이미 다시 뜬 상태라 “죽기 직전 로그”가 안 보일 수 있기 때문입니다.

kubectl -n <namespace> logs <pod-name> -c <container-name> --previous

만약 멀티 컨테이너 Pod라면 -c를 꼭 명시하세요. 사이드카는 멀쩡하고 앱 컨테이너만 죽는 경우가 흔합니다.

1-3. Deployment/ReplicaSet 이벤트도 같이 보기

Probe 실패나 이미지 풀, 스케줄링 문제는 Pod 단위보다 상위 리소스 이벤트가 더 명확할 때가 있습니다.

kubectl -n <namespace> describe deploy <deploy-name>
kubectl -n <namespace> get rs
kubectl -n <namespace> describe rs <rs-name>

2) 원인별 패턴: 로그·이벤트로 구분하는 법

CrashLoopBackOff의 원인은 크게 아래로 묶을 수 있습니다.

  1. 앱이 즉시 종료(설정 오류, 의존성 실패, 권한/자격증명 문제)
  2. 리소스 문제(OOMKilled, CPU throttling, ephemeral storage)
  3. Probe가 컨테이너를 죽임(특히 liveness)
  4. 신호 처리/Graceful shutdown 미흡으로 재시작 루프
  5. 노드/런타임/이미지 문제(드물지만 치명적)

이 중 이 글은 로그와 Probe 관점에서 자주 터지는 케이스를 집중적으로 다룹니다.

3) 앱이 즉시 종료하는 케이스: 설정/의존성/권한

3-1. 환경변수/ConfigMap/Secret 누락

증상:

  • Exit Code: 1 또는 Exit Code: 2
  • 로그에 missing env / config not found / failed to load

대표적으로 Spring Boot, Node.js, Go 모두 “필수 환경변수 없으면 즉시 종료” 패턴이 많습니다.

확인:

kubectl -n <namespace> get pod <pod-name> -o yaml | sed -n '1,200p'
kubectl -n <namespace> get configmap
kubectl -n <namespace> get secret

해결:

  • 필수 키를 ConfigMap/Secret에 추가
  • 앱이 “필수 설정 누락”일 때는 CrashLoop로 들어가므로, 가능하면 앱에서 명확한 에러 메시지를 남기고 종료하도록 유지
  • 운영에서는 optional: true를 남발하지 말고, 누락 시 조기에 실패하도록 설계하는 편이 디버깅에 유리

3-2. 외부 의존성(DB/Redis/Kafka) 연결 실패로 즉시 종료

증상:

  • 로그에 connection refused, timeout, no such host
  • readiness/liveness 이전에 앱이 종료

이 경우 “앱이 의존성 연결 실패 시 즉시 종료하는 설정”인지 확인해야 합니다. 운영에서는 보통 다음 중 하나로 정리합니다.

  • 앱은 살아있되 readiness만 실패(트래픽 차단)
  • 또는 initContainer로 의존성 준비를 기다린 뒤 앱 시작

패턴 A: initContainer로 의존성 대기

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      initContainers:
        - name: wait-db
          image: busybox:1.36
          command: ["sh", "-c", "until nc -z -w 2 db 5432; do echo waiting db; sleep 2; done"]
      containers:
        - name: app
          image: myrepo/app:1.0.0
          ports:
            - containerPort: 8080
  • 장점: 앱이 “연결 실패 즉시 종료” 정책이어도 루프를 줄일 수 있음
  • 단점: 의존성이 영구 장애면 Pod가 영구 Pending처럼 보일 수 있음(운영 정책에 따라 선택)

패턴 B: startupProbe로 “부팅 시간/의존성 준비”를 보호

아래 4장에서 자세히 다룹니다.

3-3. 클라우드 권한/자격증명 문제로 종료

EKS에서 특히 흔합니다.

  • S3 접근 시 403 또는 STS 토큰 문제
  • AWS SDK가 자격증명을 못 찾고 종료

이런 경우 앱 로그는 보통 빠르게 찍히지만, 운영자가 놓치기 쉬운 건 “Probe가 죽인 게 아니라 앱이 먼저 종료했다”는 점입니다.

관련 케이스는 아래 글들도 참고하세요.

4) Probe가 CrashLoop를 만든다: liveness/readiness/startup의 오해

Probe는 “헬스체크”가 아니라 Kubelet이 컨테이너를 재시작/트래픽 제외하는 정책입니다.

  • readinessProbe 실패: 트래픽만 제외(재시작은 하지 않음)
  • livenessProbe 실패: 컨테이너를 죽이고 재시작(= CrashLoop 유발 가능)
  • startupProbe 실패: 초기 부팅 구간에서만 실패를 허용/제어(부팅이 느린 앱 필수)

4-1. 가장 흔한 실수: 느린 부팅인데 liveness를 너무 일찍 때림

증상:

  • 이벤트에 Liveness probe failed 반복
  • 앱 로그를 보면 아직 마이그레이션/캐시 워밍업 중
  • 재시작 때문에 부팅이 끝나지 못하고 루프

해결의 핵심은 startupProbe 도입입니다.

잘못된 예시

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 3

부팅이 30초 걸리는데 5초부터 3번 실패하면 15초 내에 죽습니다.

권장 예시: startupProbe로 부팅 구간 보호

startupProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 24  # 최대 120초까지 부팅 허용

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3

포인트:

  • startupProbe가 성공하기 전까지는 liveness/readiness가 사실상 “잠시 무력화”되는 효과
  • readiness는 /ready처럼 “의존성 준비까지 포함한 진짜 준비 상태”를 보게 설계

4-2. readiness를 liveness로 쓰면 장애가 증폭된다

/health가 DB 연결까지 포함하고, DB가 일시적으로 느려지면 어떻게 될까요?

  • readiness 실패면 트래픽만 제외되어 버틸 수 있음
  • liveness 실패면 컨테이너가 재시작되며, 오히려 DB에 재연결 폭탄을 던짐

권장 분리:

  • liveness: 프로세스가 “죽었는지/교착인지”만 확인(가벼워야 함)
  • readiness: 외부 의존성(DB/캐시/메시지 브로커)까지 포함 가능

4-3. timeoutSeconds를 너무 빡세게 잡는 문제

특히 Java/Spring, Node, Python에서 GC, 이벤트 루프 지연, 콜드 캐시 등으로 헬스 엔드포인트가 간헐적으로 1초를 넘는 경우가 있습니다.

  • timeoutSeconds: 1은 운영에서 생각보다 공격적입니다.
  • 최소 2초, 보통 2초에서 5초 사이가 현실적입니다.

4-4. execProbe에서 셸 스크립트가 실패하는 케이스

exec probe는 디버깅이 어렵습니다. 스크립트가 set -e로 인해 예상치 못한 종료를 하거나, PATH 문제로 커맨드를 못 찾는 일이 잦습니다.

예시:

livenessProbe:
  exec:
    command: ["sh", "-c", "curl -sf http://127.0.0.1:8080/health"]
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

개선 팁:

  • 가능하면 httpGet을 우선 고려
  • curl을 이미지에 포함하지 않는다면 probe는 항상 실패
  • wget/curl 의존성 자체가 장애 포인트가 될 수 있으니 최소화

5) 로그가 안 남는 크래시: 즉시 종료/시그널/표준출력 문제

5-1. 애플리케이션이 너무 빨리 죽어 로그가 잘리는 경우

  • kubectl logs --previous로도 빈 로그가 나올 수 있음
  • 프로세스가 시작하자마자 크래시하거나, stdout flush 전에 종료

대응:

  • 앱 로거를 stdout로 내보내고, 버퍼링을 줄이기
  • 시작 직후 핵심 설정을 한 줄이라도 출력(버전, 환경, 필수 설정 로딩 결과)

5-2. SIGTERM 처리 미흡으로 종료가 꼬이는 경우

Kubernetes는 종료 시 SIGTERM을 보내고 terminationGracePeriodSeconds 동안 기다립니다. 앱이 SIGTERM을 무시하거나, 종료 훅이 너무 오래 걸리면 다음 배포/스케일링에서 연쇄 문제가 생길 수 있습니다.

점검:

  • preStop 훅이 과도하게 길지 않은지
  • graceful shutdown이 실제로 동작하는지

예시:

terminationGracePeriodSeconds: 30
containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 5"]

preStop은 “정말 필요한 최소 시간”만 사용하세요. 불필요한 sleep은 장애 시 복구 시간을 늘립니다.

6) CrashLoopBackOff에서 자주 보이는 이벤트 메시지별 처방

6-1. Back-off restarting failed container

의미:

  • 컨테이너가 계속 죽어서 Kubelet이 재시작 간격을 늘리는 중

처방:

  • describe pod에서 종료 코드와 reason 확인
  • logs --previous로 직전 로그 확보
  • Probe 이벤트가 있다면 4장 방식으로 조정

6-2. Liveness probe failed

의미:

  • 앱이 “죽지 않았을 수도 있는데” liveness가 죽이고 있음

처방:

  • startupProbe 도입 또는 initialDelaySeconds/failureThreshold 완화
  • liveness 엔드포인트를 더 가볍게 분리
  • timeout을 현실적으로 늘리기

6-3. Readiness probe failed

의미:

  • 트래픽만 제외되는 중(크래시 원인은 아닐 수 있음)

처방:

  • readiness가 외부 의존성을 포함한다면, 의존성 장애 시 서비스가 “정상적으로 제외”되는지 확인
  • readiness 실패가 장기화되면 HPA/롤링업데이트에서 영향을 받을 수 있으니 원인(의존성, DNS, 네트워크 정책)을 추적

7) 실전 디버깅 체크리스트(명령어 모음)

아래 순서대로 보면 대부분의 케이스가 정리됩니다.

# 1) 현재 상태 요약
kubectl -n <namespace> get pod <pod-name> -o wide

# 2) 이벤트/종료 코드/Probe 실패 확인
kubectl -n <namespace> describe pod <pod-name>

# 3) 직전 크래시 로그 확보
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous

# 4) 현재 컨테이너 로그(재시작 이후)
kubectl -n <namespace> logs <pod-name> -c <container-name>

# 5) 배포 리소스 이벤트 확인
kubectl -n <namespace> describe deploy <deploy-name>

# 6) (필요 시) 컨테이너 내부에서 헬스 엔드포인트 직접 확인
kubectl -n <namespace> exec -it <pod-name> -c <container-name> -- sh
# inside
# curl -v http://127.0.0.1:8080/health

exec가 안 될 정도로 컨테이너가 빨리 죽으면, 임시로 command를 바꿔 “살아있게” 만든 뒤 내부를 보는 방법도 있습니다.

containers:
  - name: app
    image: myrepo/app:1.0.0
    command: ["sh", "-c", "sleep 3600"]

이후 exec로 들어가 설정 파일, 환경변수, DNS, 네트워크를 점검합니다. 원인 파악 후 원래 커맨드로 복구하세요.

8) 재발 방지 설계: Probe와 관측성을 같이 맞추기

CrashLoopBackOff를 “한 번 고치고 끝”내려면, Probe와 로그/메트릭이 함께 정리되어야 합니다.

  • liveness는 가볍게, readiness는 의존성 포함 가능, 부팅은 startupProbe로 보호
  • 헬스 엔드포인트는 실패 이유를 구분 가능하게(예: 캐시 미연결, DB 타임아웃)
  • 애플리케이션 시작 시 필수 설정/버전을 한 줄로 출력
  • 종료 코드/시그널을 로그에 남기기

특히 EKS 환경에서 네트워크/의존성 문제는 gRPC 같은 프로토콜에서 “간헐적 실패”로 보이다가, 잘못된 liveness로 인해 재시작 폭풍으로 번지는 경우가 있습니다. 관련 사례는 아래 글도 참고할 만합니다.

9) 마무리: CrashLoopBackOff는 ‘증상’이고, 답은 로그와 Probe에 있다

정리하면, CrashLoopBackOff 대응의 핵심은 아래 3줄입니다.

  1. kubectl describe pod로 종료 코드와 이벤트를 먼저 본다
  2. kubectl logs --previous로 “죽기 직전 로그”를 확보한다
  3. Probe는 startupProbe를 중심으로 부팅 구간을 보호하고, liveness를 가볍게 만든다

이 3가지만 습관화해도 “원인 파악에 몇 시간 쓰던” CrashLoopBackOff가 대부분 10분 내로 정리됩니다.