Published on

Kubernetes CrashLoopBackOff 원인 7가지·즉시복구

Authors

운영 중인 파드가 CrashLoopBackOff 로 떨어지면 대부분 “컨테이너가 계속 죽고, kubelet이 재시작을 반복하지만 점점 재시작 간격(백오프)이 늘어나는 상태”입니다. 문제는 원인이 애플리케이션 버그일 수도, 설정 실수일 수도, 리소스/스토리지/프로브 같은 쿠버네티스 레이어일 수도 있다는 점입니다.

이 글은 즉시 복구(서비스 영향 최소화) 를 먼저 하고, 이후 근본 원인(RCA) 를 빠르게 좁히는 순서로 정리합니다.

관련해서 클러스터에서 kubectl logs/exec 자체가 막히는 상황이라면 먼저 아래 글을 참고해 접근성부터 복구하는 게 좋습니다.

CrashLoopBackOff를 “정확히” 이해하기

  • CrashLoopBackOff 는 에러 원인이 아니라 증상입니다.
  • 컨테이너가 종료 코드와 함께 종료되고, kubelet이 재시작 정책에 따라 재기동합니다.
  • 재시작이 반복되면 BackOff 로 재시작 간격이 점점 늘어납니다.

따라서 핵심은 두 가지입니다.

  1. 왜 종료되는가 (exit code, signal, OOM, 설정 오류 등)
  2. 왜 kubelet이 죽었다고 판단했는가 (liveness probe, startup probe, 프로세스 종료 등)

즉시 복구 우선순위(장애 대응 플레이북)

아래 순서대로 보면 “지금 당장 트래픽을 살릴 수 있는지”와 “원인을 가장 빨리 드러내는지”를 동시에 만족합니다.

1) 이벤트로 1차 분류: describe 가 가장 빠르다

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

인라인에서 <namespace><pod-name> 은 반드시 백틱으로 감싼 형태로 사용하세요.

Events 에서 다음 키워드를 먼저 찾습니다.

  • Back-off restarting failed container
  • Liveness probe failed
  • Readiness probe failed
  • OOMKilled
  • Error: ImagePullBackOff (이건 CrashLoopBackOff 이전 단계일 때도 많음)
  • MountVolume.SetUp failed

2) 로그 확보: “현재 로그”와 “이전 로그”를 둘 다 본다

CrashLoop는 프로세스가 빨리 죽기 때문에 이전 컨테이너 로그가 결정적입니다.

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

3) 롤아웃 즉시 되돌리기(가장 안전한 즉시복구)

최근 배포 이후부터 발생했다면 원인 분석보다 먼저 롤백으로 서비스부터 복구하는 편이 낫습니다.

kubectl -n <namespace> rollout history deploy/<deploy-name>
kubectl -n <namespace> rollout undo deploy/<deploy-name>

4) 프로브로 인해 죽는다면 “일시 완화”로 숨통 틔우기

Liveness probe failed 로 계속 재시작한다면, 짧은 시간이라도 프로브를 완화해 원인 분석 시간을 벌 수 있습니다.

  • startupProbe 를 추가하거나
  • livenessProbe.initialDelaySeconds 를 늘리거나
  • failureThreshold 를 늘립니다.
kubectl -n <namespace> patch deploy/<deploy-name> --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/initialDelaySeconds","value":60},
  {"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/failureThreshold","value":10}
]'

주의: 이 조치는 “근본 해결”이 아니라 재시작 루프를 잠깐 멈추는 응급처치입니다.

5) 리소스 이슈가 의심되면 즉시 상향(특히 메모리)

OOMKilled 가 보이면 “원인 분석” 전에 메모리를 올려 서비스부터 살릴 수 있습니다.

kubectl -n <namespace> set resources deploy/<deploy-name> \
  --containers=<container-name> \
  --limits=memory=1Gi,cpu=500m \
  --requests=memory=512Mi,cpu=200m

CrashLoopBackOff 원인 7가지(진단 포인트와 해결)

1) 애플리케이션이 즉시 종료됨(설정/환경변수/엔트리포인트 오류)

가장 흔한 케이스입니다.

  • 잘못된 ENTRYPOINT 또는 CMD
  • 필수 환경변수 누락
  • 설정 파일 경로 불일치
  • 포트 바인딩 실패

진단

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

해결 체크리스트

  • envFromConfigMap / Secret 키가 실제로 존재하는지
  • 애플리케이션이 “포그라운드”로 실행되는지(데몬화되면 컨테이너가 종료될 수 있음)
  • Dockerfile에서 CMD 가 셸 확장에 의존하지 않는지

예: 필수 환경변수 미설정 방지(헬스체크 전에 빠르게 실패시키기)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  template:
    spec:
      containers:
        - name: app
          image: myapp:1.0.0
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secret
                  key: databaseUrl

2) OOMKilled(메모리 부족) 또는 과도한 GC/캐시

describe 의 컨테이너 상태에 OOMKilled 가 찍히면 거의 확정입니다.

진단

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

즉시복구

  • 메모리 limit 상향
  • 워커 수/동시성 하향
  • 캐시/버퍼 크기 축소

근본해결

  • 언어 런타임 옵션 조정(예: Node.js --max-old-space-size)
  • 메모리 릭 분석
  • HPA 기준을 CPU만 보지 말고 메모리도 고려

Java/Spring 계열에서 DB 커넥션/스레드가 누수나 대기 폭증을 유발하면 간접적으로 메모리 압박이 커질 수 있습니다. 관련 진단 패턴은 아래 글도 참고할 만합니다.

3) Liveness/Startup/Readiness 프로브 설정 실패

프로브 실패는 “프로세스는 살아있는데 kubelet이 죽였을” 가능성이 큽니다.

전형적인 실수

  • 앱 부팅이 느린데 startupProbe 없이 livenessProbe 만 둠
  • timeoutSeconds 가 너무 짧음
  • HTTP 헬스 엔드포인트가 의존성(DB, 외부 API)까지 강하게 결합되어 일시 장애에 취약

진단

kubectl -n <namespace> describe pod <pod-name>
# Events에서 probe failed 메시지 확인

권장 설정 예시(느린 부팅 앱)

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

포인트

  • startupProbe 는 “부팅 완료 전까지 liveness를 유예”하는 용도
  • readinessProbe 는 “트래픽 투입 여부”만 제어하고, 보통 재시작과는 직접 연결되지 않음

4) 이미지/아키텍처/런타임 불일치(실행 파일이 안 뜸)

  • exec format error (예: ARM 노드에 AMD64 바이너리)
  • 라이브러리 누락(동적 링크 실패)
  • 컨테이너가 시작하자마자 크래시

진단

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

해결

  • 멀티 아키텍처 이미지 빌드(linux/amd64, linux/arm64)
  • distroless 사용 시 필요한 동적 라이브러리 포함 여부 확인
  • command / args 로 엔트리포인트가 덮어써지지 않았는지 점검

5) ConfigMap/Secret/볼륨 마운트 문제로 프로세스가 실패

파일 기반 설정을 읽는 앱에서 흔합니다.

  • 키가 없어서 파일이 비어 있음
  • 마운트 경로가 앱 기대와 다름
  • 권한 문제로 읽기 실패

진단

kubectl -n <namespace> describe pod <pod-name>
kubectl -n <namespace> get cm <configmap-name> -o yaml
kubectl -n <namespace> get secret <secret-name> -o yaml

즉시복구 팁

  • 최근에 ConfigMap만 바꿨다면 “Deployment 재시작”으로 반영
kubectl -n <namespace> rollout restart deploy/<deploy-name>

근본해결

  • 설정 파일 경로를 코드/차트에서 상수화
  • 필수 키 존재 여부를 CI에서 검증

6) 의존성(DB, Kafka, 외부 API) 연결 실패를 “치명적 종료”로 처리

앱이 부팅 중 DB 연결 실패를 만나면 그대로 exit(1) 하는 패턴이 있습니다. 그러면 외부 의존성의 일시 장애가 곧바로 CrashLoop로 번집니다.

진단

  • 로그에 connection refused, timeout, authentication failed 같은 메시지 확인

즉시복구

  • 의존성 복구가 먼저(네트워크, SG, DNS, 인증서)
  • 앱이 “재시도(backoff) 후 계속 실행”하도록 동작 변경

근본해결

  • 부팅 시 의존성 체크를 readinessProbe 로 옮기고, 프로세스는 살아 있게 유지
  • 서킷 브레이커/재시도 정책 도입

Ingress/ALB 레벨 타임아웃이 겹치면 장애가 더 커 보일 수 있습니다. 트래픽 계층의 타임아웃 진단은 아래 글도 함께 보면 원인 분리가 빨라집니다.

7) 노드/런타임/권한(SecurityContext) 문제

앱이 특정 권한을 요구하거나(바인딩, 파일 쓰기), 런타임 제약이 강할 때 발생합니다.

  • runAsNonRoot 인데 이미지가 root 전제
  • readOnlyRootFilesystem: true 인데 앱이 /tmp 외 경로에 쓰기
  • fsGroup 미설정으로 볼륨 접근 불가

진단

kubectl -n <namespace> describe pod <pod-name>
# permission denied, operation not permitted 같은 로그/이벤트 확인

해결 예시

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

그리고 쓰기가 필요하면 emptyDir/tmp 등에 마운트합니다.

volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}

재현/분석이 어려울 때: 디버그 컨테이너로 “살아있는 쉘” 확보

CrashLoop는 컨테이너가 너무 빨리 죽어 exec 가 어려울 수 있습니다. 이때는 kubectl debug 로 임시 디버그 컨테이너를 붙여 네트워크/DNS/볼륨을 확인합니다.

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

확인할 것

  • DNS: nslookup <service-name>
  • 라우팅: wget -S -O - http://<service-name>:<port>/health
  • 파일: 설정 마운트 경로 존재 여부

운영에서 바로 쓰는 “10분 내 결론” 체크리스트

  • kubectl describe pod 이벤트에서 OOMKilled 또는 probe failed 를 먼저 찾는다
  • kubectl logs --previous 로 직전 크래시 원인을 확보한다
  • 최근 배포 직후면 rollout undo 로 즉시 롤백한다
  • 프로브가 원인이면 startupProbe 도입 또는 임시 완화로 재시작 루프를 끊는다
  • 메모리면 limit 상향으로 먼저 살리고, 이후 릭/동시성/GC를 분석한다
  • 의존성 실패를 부팅 치명 종료로 처리하지 않도록 readiness 중심으로 재설계한다

마무리: “응급처치”와 “근본해결”을 분리하자

CrashLoopBackOff 대응에서 가장 중요한 건, 원인을 찾기 전에 서비스를 살릴 수 있는 조치(롤백, 리소스 상향, 프로브 완화)로 가용성을 먼저 확보하고, 그 다음 로그/이벤트/exit code 기반으로 원인을 좁혀 재발 방지로 이어가는 것입니다.

특히 프로브와 리소스는 애플리케이션 코드가 멀쩡해도 운영에서 흔히 깨지는 지점이므로, 배포 파이프라인에 describe 이벤트 점검과 프로브/리소스 정책 리뷰를 정례화하면 CrashLoopBackOff의 상당수를 예방할 수 있습니다.