Published on

Kubernetes CrashLoopBackOff 원인 7가지와 실전 디버깅

Authors

서버가 죽었다는 알림보다 더 사람을 지치게 하는 신호가 CrashLoopBackOff 입니다. 단순히 재시작이 반복되는 상태처럼 보이지만, 실제로는 컨테이너가 짧은 시간 안에 종료되고, kubelet이 백오프를 걸며 재시작 간격을 늘리는 과정에서 나타나는 증상 이름에 가깝습니다.

이 글은 CrashLoopBackOff를 "원인별로 분류하고, 가장 빨리 확인할 것부터" 순서대로 디버깅하는 실전 루틴에 초점을 맞춥니다. 특히 로그가 안 남거나, 컨테이너가 너무 빨리 죽어서 kubectl exec가 실패하는 케이스까지 포함합니다.

CrashLoopBackOff를 이해하는 최소 개념

  • 컨테이너가 종료되면 kubelet이 restartPolicy에 따라 재시작합니다.
  • 짧은 주기로 계속 실패하면 kubelet은 재시작 간격을 늘리는 백오프를 적용합니다.
  • 따라서 핵심은 "왜 컨테이너가 종료되는가"이며, 종료 코드, 이벤트, 프로브 결과가 단서입니다.

먼저 아래 4가지는 습관처럼 확인하세요.

# 1) 파드 상태/컨테이너 상태 요약
kubectl get pod -n <namespace> <pod-name> -o wide

# 2) 종료 코드/Reason/메시지까지 자세히
kubectl describe pod -n <namespace> <pod-name>

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

# 4) 이벤트만 따로 빠르게
kubectl get events -n <namespace> --sort-by=.lastTimestamp | tail -n 30

kubectl describeState, Last State, Exit Code, Reason, Events는 원인 분류에 결정적입니다.

원인 1: 애플리케이션 예외로 즉시 종료 (ExitCode 1)

가장 흔합니다. 설정 누락, 환경변수 파싱 실패, 외부 의존성 연결 실패 등으로 프로세스가 시작 직후 예외를 던지고 끝납니다.

빠른 확인 포인트

  • kubectl logs --previous에서 스택트레이스가 보이는지
  • Exit Code: 1 또는 언어 런타임이 남긴 에러가 있는지
kubectl logs -n <namespace> <pod-name> -c <container-name> --previous | tail -n 200

자주 놓치는 패턴

  • 설정 파일 경로가 컨테이너 이미지 내에서 다름
  • ConfigMap 키 이름 오타로 null이 들어옴
  • DATABASE_URL 같은 필수 환경변수가 비어 있음

해결 접근

  • 로컬과 동일한 실행 커맨드인지 spec.containers.commandargs 확인
  • 설정을 읽는 부분에 "필수값 누락 시 어떤 값이 비었는지"를 로그로 남기기

원인 2: 이미지/엔트리포인트/권한 문제로 실행 자체가 실패

CrashLoopBackOff는 "컨테이너가 뜬 다음 죽는" 경우가 많지만, 엔트리포인트가 실행되지 못해도 비슷한 증상으로 이어질 수 있습니다.

체크리스트

  • kubectl describeEventsError: failed to start container 류가 있는지
  • exec format error (아키텍처 불일치) 여부
  • 실행 파일 권한 (permission denied) 여부

예를 들어 Alpine 기반 이미지에서 실행 파일 권한이 빠지면 다음처럼 죽습니다.

# 잘못된 예: 권한 부여 누락
COPY myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]
# 수정 예
COPY myapp /usr/local/bin/myapp
RUN chmod +x /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

또는 Apple Silicon에서 빌드한 이미지를 amd64 노드에 올리면 exec format error가 납니다. 이 경우 멀티아키 빌드가 필요합니다.

원인 3: Liveness/Readiness/Startup 프로브 오판정

프로브가 잘못되면 애플리케이션은 정상인데도 kubelet이 "죽었다"고 판단하고 재시작을 걸 수 있습니다. 특히 Startup Probe를 쓰지 않거나, 초기 기동이 느린데 Liveness가 너무 공격적인 경우가 많습니다.

증상

  • 로그상 애플리케이션은 정상 기동 중인데, 일정 시간마다 재시작
  • kubectl describe 이벤트에 Liveness probe failed 또는 Readiness probe failed 반복
kubectl describe pod -n <namespace> <pod-name> | sed -n '1,200p'

실전 권장 설정

  • 초기 부팅이 느리면 startupProbe를 추가하고, 그 동안 livenessProbe를 유예
  • HTTP 기반이면 타임아웃과 실패 임계치를 현실적으로
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

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

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

프로브는 "장애 감지"가 목적이지 "성능 테스트"가 목적이 아닙니다. 너무 타이트하면 정상 트래픽 변동에서도 재시작이 발생합니다.

참고로 헬스체크 관점에서의 진단 루틴은 ALB에서도 유사합니다. 네트워크 경로와 헬스체크 조건을 분리해서 보려면 AWS ALB 502/504 폭증 시 TargetGroup 헬스체크 진단도 함께 보면 좋습니다.

원인 4: OOMKilled (메모리 부족) 또는 메모리 제한 설정 오류

Exit Code: 137과 함께 Reason: OOMKilled가 보이면 거의 확정입니다.

확인

kubectl describe pod -n <namespace> <pod-name> | grep -n "OOM" -n

# 노드/파드 리소스 현황
kubectl top pod -n <namespace>
kubectl top node

흔한 실수

  • resources.limits.memory를 너무 낮게 잡음
  • JVM/Node.js 런타임 힙 설정이 컨테이너 메모리 제한보다 큼
  • 순간 피크(캐시 워밍, 대용량 응답 압축, 배치 작업) 고려 누락

해결

  • 우선 requestslimits를 현실적으로 조정
  • JVM이라면 -XX:MaxRAMPercentage 또는 힙 상한을 제한에 맞춤
  • Node.js라면 --max-old-space-size를 제한에 맞게 조정
resources:
  requests:
    cpu: "200m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1024Mi"

원인 5: Secret/ConfigMap/볼륨 마운트 문제로 기동 실패

애플리케이션이 시작하면서 특정 파일을 읽는데, 마운트가 실패했거나 키가 누락되면 즉시 종료할 수 있습니다.

체크 포인트

  • kubectl describe 이벤트에 MountVolume.SetUp failed가 있는지
  • ConfigMap 키가 존재하는지
  • subPath 사용 시 파일이 실제로 생성되는지
kubectl describe pod -n <namespace> <pod-name> | grep -n "Mount" -n
kubectl get configmap -n <namespace> <configmap-name> -o yaml
kubectl get secret -n <namespace> <secret-name> -o yaml

실전 팁

  • envFrom로 통째로 주입할 때는 키 충돌과 이름 변경에 취약합니다.
  • 중요한 설정은 "필수 키 검증"을 애플리케이션 시작 단계에서 명시적으로 하고, 어떤 키가 누락됐는지 로그로 남기세요.

원인 6: 의존성(DB, Kafka, 외부 API) 연결 실패로 프로세스 종료

Kubernetes 자체 문제라기보다, 네트워크/권한/엔드포인트 문제로 애플리케이션이 "실패하면 즉시 종료"하도록 구성돼 있을 때 CrashLoop로 보입니다.

대표 케이스

  • DB 연결 실패 시 재시도 없이 종료
  • IAM/IRSA 권한 문제로 AWS API 호출이 403으로 실패하고 종료
  • 사설 DNS/서비스 디스커버리 문제

EKS에서 특히 많이 보는 것이 IRSA 또는 AWS 권한 이슈입니다. 파드에서 S3 접근이 403으로 터지며 애플리케이션이 종료하는 경우는 EKS Pod에서 S3 403 AccessDenied 원인 10가지 체크리스트가 그대로 도움이 됩니다.

디버깅 접근

  • 애플리케이션이 "의존성 실패 시 종료"하도록 돼 있다면, 재시도/서킷브레이커/지수 백오프를 넣고 프로세스는 살아있게 설계하는 편이 운영에 유리합니다.
  • 네트워크 레벨 확인을 위해 임시 디버그 파드를 띄워 동일한 네임스페이스/서비스어카운트로 테스트합니다.
kubectl run -n <namespace> net-debug \
  --image=ghcr.io/nicolaka/netshoot:latest \
  --restart=Never \
  --overrides='{"spec":{"serviceAccountName":"<sa-name>"}}'

kubectl exec -n <namespace> -it net-debug -- sh
# 내부에서 DNS/포트 확인
nslookup <service-name>
curl -v http://<service-name>:<port>/healthz

원인 7: SIGTERM 처리/종료 훅(preStop) 문제로 재시작 루프가 증폭

이건 "즉시 크래시"라기보다, 롤링 업데이트나 노드 드레인 상황에서 종료가 꼬여 재시작이 연쇄적으로 발생하는 패턴입니다.

흔한 문제

  • preStop 훅이 너무 오래 걸려 terminationGracePeriodSeconds를 초과
  • SIGTERM을 받자마자 즉시 종료하며, 처리 중이던 작업이 깨져 다음 기동에서 또 실패
  • 애플리케이션이 종료 시 파일/락을 정리하지 못해 다음 기동에서 에러
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 30

해결은 단순히 유예 시간을 늘리는 것만이 아니라, 애플리케이션이 SIGTERM을 받았을 때 "신규 요청 차단, 진행 중 요청 정리, 리소스 정리"를 제대로 하도록 만드는 것입니다.

실전 디버깅 루틴: 빠르게 원인에 도달하는 순서

현장에서는 원인 7가지를 "한 번에" 찾기보다, 비용이 낮은 관측부터 순차적으로 좁혀가면 됩니다.

1단계: 종료 코드와 Reason으로 분기

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

kubectl get pod -n <namespace> <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
  • OOMKilled면 리소스부터
  • Error면 로그와 이벤트부터
  • Completed인데 루프면 커맨드/잡 성격 여부 확인

2단계: --previous 로그로 직전 크래시 원인 확보

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

로그가 비어있다면 다음을 의심합니다.

  • stdout으로 로그를 안 찍고 파일로만 남김
  • 너무 빨리 죽어서 로그 플러시가 안 됨
  • 애초에 엔트리포인트가 실행되지 못함

3단계: 이벤트에서 프로브/마운트/스케줄링 이슈 확인

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

특히 Events는 신뢰도가 높습니다.

  • Liveness probe failed
  • Back-off restarting failed container
  • MountVolume.SetUp failed

4단계: 컨테이너가 너무 빨리 죽을 때의 강제 디버깅

컨테이너가 즉시 종료하면 kubectl exec가 어려워집니다. 이때는 "프로세스를 살려두는" 임시 패치를 적용해 원인을 캡처합니다.

예: 엔트리포인트를 잠깐 sleep으로 바꿔서 내부를 확인

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

그 다음 파드에 들어가 파일/환경변수/네트워크를 확인합니다.

kubectl exec -n <namespace> -it <pod-name> -- sh
env | sort
ls -al
cat /etc/resolv.conf

원인 확인 후에는 반드시 원래 커맨드로 되돌리고 롤아웃합니다.

5단계: 재발 방지 체크

  • 프로브를 "기동 시간"과 "피크 부하"를 고려해 조정
  • 필수 설정 검증을 애플리케이션 시작 시 명확히
  • 의존성 실패 시 즉시 종료 대신 재시도/백오프 전략 도입
  • 메모리 제한과 런타임 힙 설정 정합성 확보

자주 묻는 질문

CrashLoopBackOff와 ImagePullBackOff는 무엇이 다른가

  • ImagePullBackOff는 이미지 다운로드 단계에서 막힌 상태입니다.
  • CrashLoopBackOff는 컨테이너가 실행된 뒤(또는 실행 시도 후) 반복 실패로 백오프가 걸린 상태입니다.

파드 하나만 크래시하고 다른 파드는 정상인 이유

  • 특정 노드의 리소스 압박, 특정 AZ 네트워크, 노드 로컬 캐시, 혹은 특정 파드에만 주입된 설정 차이(Secret 롤링, 잘못된 값) 등 "환경 차이" 때문인 경우가 많습니다.

이럴 때는 같은 서비스어카운트/같은 노드에 스케줄링해 재현해보거나, 문제 파드와 정상 파드의 환경변수/마운트/이미지 태그를 비교하세요.

마무리

CrashLoopBackOff는 공포의 상태값처럼 보이지만, 실은 단서가 꽤 잘 남습니다. 종료 코드, --previous 로그, 이벤트, 프로브 설정만 체계적으로 보면 대부분 10분 안에 원인 범주를 좁힐 수 있습니다.

특히 EKS 환경에서는 애플리케이션이 AWS API 호출 실패를 치명적으로 처리하면서 크래시로 이어지는 경우가 흔하니, 권한과 네트워크를 함께 점검하는 습관이 효과적입니다. 위 7가지 원인을 체크리스트로 만들어 두면, 다음 번 CrashLoopBackOff는 "장애"가 아니라 "작업"이 됩니다.