Published on

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

Authors

서론

CrashLoopBackOff는 Kubernetes가 컨테이너를 시작했지만 곧바로 종료(exit)되어 재시작을 반복할 때 나타나는 상태입니다. 중요한 점은 CrashLoopBackOff가 ‘원인’이 아니라 증상이라는 것입니다. 같은 CrashLoopBackOff라도 실제 원인은 다음처럼 전혀 다를 수 있습니다.

  • 애플리케이션이 즉시 종료(설정 누락, 예외, 마이그레이션 실패)
  • livenessProbe가 과격해 정상 프로세스를 죽임
  • OOMKilled(메모리 부족)로 커널이 종료
  • 이미지/엔트리포인트/권한 문제로 프로세스가 뜨지 못함
  • 의존 서비스(DB/Redis/EFS 등) 연결 타임아웃으로 스타트업 실패

이 글은 로그/이벤트/프로브 중심의 진단 루틴을 먼저 제시하고, 흔한 원인별로 “무엇을 보면 확정할 수 있는지”와 “어떻게 고치면 재발을 막는지”를 정리합니다.


1) 가장 먼저 보는 것: 상태, 이벤트, 이전 로그

CrashLoopBackOff는 재시작이 반복되므로 현재 로그(kubectl logs)만 보면 원인 로그가 이미 날아간 경우가 많습니다. 그래서 아래 순서가 안전합니다.

1-1. Pod 상태/컨테이너 종료 사유 확인

kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>

describe에서 특히 아래를 봅니다.

  • Last State: TerminatedReason, Exit Code, Finished
  • Events 섹션의 Back-off restarting failed container
  • OOMKilled, Error, Completed 여부

1-2. “이전(previous)” 로그가 핵심

컨테이너가 재시작된 상황이면 이전 인스턴스 로그를 봐야 합니다.

# 현재 실행 중인 컨테이너 로그
kubectl logs -n <ns> <pod> -c <container>

# 직전 종료된 컨테이너 로그(가장 중요)
kubectl logs -n <ns> <pod> -c <container> --previous

1-3. 재시작 횟수/패턴으로 원인 좁히기

kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].restartCount}'
  • 수 초 단위로 바로 죽으면: 엔트리포인트/설정/권한/즉시 예외 가능성
  • 30~60초 등 일정 지연 후 죽으면: 프로브(liveness) 또는 스타트업 타임아웃 가능성
  • 부하가 걸린 뒤 죽으면: OOM/스레드 폭증/외부 의존성 타임아웃 가능성

2) 원인 A: 애플리케이션이 즉시 종료(exit 1/2/…)

가장 흔한 유형입니다. 컨테이너는 정상 실행되지만 앱이 내부 오류로 종료합니다.

2-1. 확인 포인트

  • kubectl logs --previous에 스택트레이스/환경변수 누락/설정 파일 미존재
  • Exit Code: 1(일반 오류), Exit Code: 2(잘못된 인자) 등

2-2. 자주 나오는 케이스

  • 환경변수(예: DATABASE_URL) 미설정
  • ConfigMap/Secret 키 이름 오타
  • 마이그레이션 실패로 프로세스 종료
  • 앱이 “foreground”가 아니라 즉시 종료되는 방식으로 실행됨

2-3. 해결 패턴

(1) ConfigMap/Secret 키를 명시적으로 검증

앱 시작 시 필수 설정을 검사하고, 누락 시 명확한 로그를 남기도록 합니다.

(2) 엔트리포인트가 즉시 종료되지 않는지 확인

예: Dockerfile에서 CMD ["sh", "-c", "python init.py"]처럼 init만 하고 끝나면 컨테이너도 종료됩니다. 메인 프로세스를 실행해야 합니다.


3) 원인 B: OOMKilled (메모리 부족)

메모리 부족은 CrashLoopBackOff의 대표 원인입니다. 특히 JVM/Node/Python에서 초기 로딩, 캐시 워밍, 대용량 파일 처리 시 잘 터집니다.

3-1. 확인 포인트

kubectl describe pod에서:

  • Last State: TerminatedReason: OOMKilled
  • 이벤트에 Killed 관련 메시지

또는 아래로도 빠르게 확인 가능합니다.

kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'

3-2. 해결 패턴

(1) requests/limits 재설정

resources:
  requests:
    cpu: "250m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"
  • requests는 스케줄링 기준, limits는 강제 상한입니다.
  • OOM은 대개 limits.memory가 너무 낮거나, 앱 메모리 튜닝이 부족해서 발생합니다.

(2) 런타임별 메모리 상한 튜닝

  • JVM: -XX:MaxRAMPercentage, -Xms/-Xmx
  • Node.js: --max-old-space-size
  • Python: 워커 수/동시성 제한, 대용량 처리 스트리밍

4) 원인 C: livenessProbe가 정상 프로세스를 죽인다

livenessProbe는 “죽은 프로세스를 재시작”하는 장치인데, 설정이 공격적이면 살아있는 프로세스도 죽여서 CrashLoopBackOff를 만듭니다.

4-1. 확인 포인트

  • 이벤트에 Liveness probe failed 반복
  • 재시작 주기가 periodSeconds와 유사
  • 앱 로그에는 큰 오류가 없는데도 재시작됨
kubectl describe pod -n <ns> <pod> | sed -n '/Events:/,$p'

4-2. 가장 흔한 실수

  • 앱 기동이 느린데 initialDelaySeconds가 너무 짧음
  • timeoutSeconds가 너무 짧아 순간 지연에 실패
  • DB 의존 엔드포인트를 liveness로 체크(외부 장애가 곧 프로세스 kill로 이어짐)

4-3. 해결: startupProbe를 먼저 도입

기동 시간이 긴 앱은 startupProbe로 “초기 부팅 구간”을 보호하고, 이후에만 liveness를 적용하는 패턴이 안정적입니다.

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 2

livenessProbe:
  httpGet:
    path: /live
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3
  • startupProbe가 성공하기 전에는 livenessProbe가 동작하지 않습니다.
  • readinessProbe는 트래픽 유입 여부만 제어하므로, 외부 의존성(DB 등)을 포함해도 상대적으로 안전합니다.

5) 원인 D: readinessProbe 실패를 Crash로 오해하는 경우

엄밀히 말해 readiness 실패는 CrashLoopBackOff를 만들지 않습니다. 하지만 다음 상황에서는 “계속 재시작되는 것처럼” 보일 수 있습니다.

  • 상위 오케스트레이션(Argo CD/HPA/배포 스크립트)이 readiness 실패를 장애로 보고 롤백/재배포
  • 서비스가 트래픽을 못 받아서 외부에서 헬스체크 실패 → 자동 재시작 정책이 개입

GitOps 환경이라면 배포/동기화 상태도 함께 확인하는 게 좋습니다. 동기화/헬스가 꼬인 케이스는 Argo CD Sync 실패 - OutOfSync·Degraded 해결법도 같이 참고하면 진단 속도가 빨라집니다.


6) 원인 E: 의존 서비스(네트워크/스토리지) 타임아웃으로 스타트업 실패

앱이 시작 과정에서 DB/Redis/EFS 등을 붙다가 실패하면 exit → CrashLoopBackOff로 이어집니다.

6-1. 확인 포인트

  • 로그에 Connection timed out, ECONNREFUSED, DNS 실패
  • 특정 시간(예: 10분) 이후에 죽는다면 라이브러리 기본 타임아웃 가능성

예를 들어 EKS에서 Redis 연결이 특정 패턴으로 오래 걸릴 때는 네트워크 경로/보안그룹/NACL/DNS/클라이언트 타임아웃을 같이 봐야 합니다. 비슷한 진단 흐름은 EKS Pod→ElastiCache Redis 10분 타임아웃 진단법에 정리해 두었습니다.

스토리지 마운트가 원인인 경우도 많습니다. Pod는 Running으로 보이는데 컨테이너 내부는 마운트 대기/타임아웃으로 죽는 형태가 나오기도 합니다. EFS 계열은 EKS에서 Pod는 뜨는데 EFS Mount 타임아웃 해결처럼 VPC/DNS/보안그룹/마운트 타깃을 함께 점검해야 합니다.

6-2. 해결 패턴

  • 앱 스타트업에서 외부 의존성을 “필수”로 묶지 말고 재시도/backoff 적용
  • readiness에 의존성 체크를 넣고, liveness는 프로세스 생존만 판단
  • DNS 이슈 의심 시 nslookup, dig를 디버그 파드로 수행

디버그용 임시 파드 예시:

kubectl run -n <ns> net-debug --rm -it --image=busybox:1.36 -- sh
# inside
nslookup redis.my-namespace.svc.cluster.local
wget -S -O- http://my-service:8080/healthz

7) 원인 F: 잘못된 command/args, 권한, 파일 경로 문제

컨테이너가 뜨자마자 죽는데 로그도 거의 없으면 엔트리포인트/권한 문제를 의심합니다.

7-1. 확인 포인트

  • kubectl describeError: failed to start container류 메시지
  • exec format error(아키텍처 불일치), permission denied, no such file or directory

7-2. 해결 패턴

  • command/args를 이미지 기준으로 다시 확인
  • WORKDIR/상대경로 사용 시 실제 경로 존재 여부 확인
  • runAsNonRoot 사용 시 실행 파일 권한/소유자 점검

보안 컨텍스트 예시:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  readOnlyRootFilesystem: true

readOnlyRootFilesystem: true를 켠 상태에서 앱이 /tmp나 루트에 파일을 쓰면 즉시 죽을 수 있습니다. 이 경우 emptyDir로 쓰기 가능 경로를 제공해야 합니다.


8) 프로브 설계 체크리스트(재발 방지 핵심)

CrashLoopBackOff를 “고쳤다”가 아니라 “다시 안 터지게 했다”로 만들려면 프로브 설계가 중요합니다.

8-1. 역할 분리

  • startupProbe: 기동 완료까지 보호(느린 부팅/마이그레이션/캐시 워밍)
  • readinessProbe: 트래픽 받을 준비(외부 의존성 포함 가능)
  • livenessProbe: 프로세스가 살아있는지(외부 의존성 제외 권장)

8-2. HTTP 엔드포인트 추천 형태

  • /live: 단순 200(프로세스 루프/스레드 상태 등 최소)
  • /ready: DB ping, 큐 연결, 필수 리소스 준비 여부 확인
  • /healthz: 종합 상태(관측용)

8-3. 타임아웃/임계치 가이드

  • timeoutSeconds: 최소 2~5초(네트워크/GC 고려)
  • failureThreshold: 일시적 스파이크를 흡수할 정도로
  • periodSeconds: 너무 촘촘하면 장애 시 증폭

9) 실전 트러블슈팅 루틴(요약)

아래 순서로 보면 대부분 10분 내에 원인 범주를 좁힐 수 있습니다.

  1. kubectl describe podLast State, Exit Code, Events 확인
  2. kubectl logs --previous로 “죽기 직전” 로그 확보
  3. OOMKilled면 리소스/런타임 튜닝부터
  4. Liveness probe failed면 startupProbe 도입 + liveness 완화
  5. 외부 의존성 오류면 readiness로 격리 + 재시도/backoff + 네트워크/DNS 점검
  6. 엔트리포인트/권한 문제면 command/args, 아키텍처, 파일 권한 재검증

결론

CrashLoopBackOff는 Kubernetes가 친절하게 “계속 죽어서 다시 띄우고 있다”라고 알려주는 신호일 뿐, 해결의 열쇠는 **이전 로그(--previous)와 이벤트(특히 probe 실패/리소스 킬)**에 있습니다.

  • 로그로 애플리케이션 종료 원인을 확정하고
  • 이벤트로 kubelet이 왜 재시작을 유도했는지(프로브/OOM)를 확인하며
  • startup/readiness/liveness의 역할을 분리해 재발을 막으면

CrashLoopBackOff는 대부분 “한 번의 패치”가 아니라 운영 가능한 헬스체크/리소스 설계로 끝낼 수 있습니다.