본문 바로가기

Troubleshooting

Kubernetes CrashLoopBackOff 원인과 해결 방법

반응형

Deployment를 배포했는데 Pod가 Running 상태를 유지하지 못합니다. kubectl get pods를 치면 RESTARTS가 계속 올라가고, STATUS가 CrashLoopBackOff입니다. 로그를 봐도 뭐가 문제인지 바로 안 보입니다. CrashLoopBackOff는 "컨테이너가 시작 직후 종료를 반복하고 있다"는 신호이고, Exit Code와 로그를 함께 봐야 원인을 좁힐 수 있습니다.

Troubleshooting Kubernetes Level 2 15분

핵심 요약

원인 Exit Code / 키워드 확인 방법
코드 버그 / 미처리 예외 Exit 1, Exception 메시지 kubectl logs --previous
환경변수 / 설정 누락 Exit 1, config 관련 에러 ConfigMap, Secret 마운트 확인
OOMKilled Exit 137, OOMKilling 이벤트 kubectl describe pod Events
Command / Entrypoint 오류 Exit 126/127 Dockerfile ENTRYPOINT, Pod command 확인
Liveness Probe 실패 Liveness probe failed 이벤트 Probe 설정값과 앱 시작 시간 비교
의존 서비스 연결 실패 Exit 1, Connection refused 의존 서비스 상태, NetworkPolicy 확인
Volume 권한 문제 Permission denied securityContext, fsGroup 확인

1. CrashLoopBackOff란

CrashLoopBackOff는 Pod 상태(status)가 아니라, 컨테이너가 반복적으로 실패하고 있을 때 Kubernetes가 재시작 간격을 점진적으로 늘리는 동작을 나타냅니다.

컨테이너가 시작 직후 비정상 종료하면, kubelet은 restartPolicy에 따라 컨테이너를 재시작합니다. 재시작할 때마다 대기 시간을 10초 → 20초 → 40초 → ... 최대 5분까지 늘립니다(exponential backoff). 이 대기 상태가 CrashLoopBackOff입니다.

핵심 포인트:

  • CrashLoopBackOff 자체는 "재시작 대기 중"이라는 의미입니다. 실제 원인은 컨테이너 종료 사유에 있습니다.
  • 컨테이너가 10분 이상 정상 실행되면 backoff 타이머가 초기화됩니다.
  • restartPolicy: Never인 경우 CrashLoopBackOff는 발생하지 않습니다. 대신 Pod 상태가 Failed로 고정됩니다.
CrashLoopBackOff 발생 흐름
CrashLoopBackOff 발생 흐름

2. 진단 시작: 3가지 확인 포인트

CrashLoopBackOff가 발생하면 아래 세 가지를 순서대로 확인합니다.

2-1. Exit Code 확인

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

출력에서 확인할 부분:

State:          Waiting
  Reason:       CrashLoopBackOff
Last State:     Terminated
  Reason:       Error
  Exit Code:    1
  Started:      Thu, 05 Jun 2026 10:30:00 +0900
  Finished:     Thu, 05 Jun 2026 10:30:02 +0900

Exit Code가 원인을 좁히는 가장 빠른 단서입니다.

Exit Code 의미 대표 원인
0 정상 종료 프로세스가 의도대로 끝남 (데몬이 아닌 프로그램)
1 일반 에러 코드 버그, 설정 파일 누락, 의존 서비스 연결 실패
126 명령어 실행 불가 실행 파일 권한 없음 (chmod +x 필요)
127 명령어 없음 Entrypoint/Command 경로 오류, 바이너리 누락
137 SIGKILL (9) OOMKilled 또는 외부에서 강제 종료
139 SIGSEGV (11) Segmentation fault, 메모리 접근 위반
143 SIGTERM (15) 정상 종료 요청 (preStop hook, 롤링 업데이트)

Exit Code 0인데 CrashLoopBackOff?

컨테이너의 메인 프로세스가 정상 종료(Exit 0)해도, Kubernetes는 restartPolicy: Always(Deployment 기본값)에 따라 컨테이너를 다시 시작합니다. 이 패턴은 일회성 작업을 Deployment로 실행했을 때 발생합니다. 배치 작업은 Job이나 CronJob을 사용해야 합니다.

2-2. 이전 컨테이너 로그 확인

CrashLoopBackOff 상태에서 kubectl logs를 실행하면 "현재 재시작 중인" 컨테이너의 로그가 나옵니다. 이미 종료된 이전 컨테이너의 로그를 봐야 실제 에러를 확인할 수 있습니다.

# 이전(종료된) 컨테이너 로그 확인
kubectl logs <pod-name> --previous -n <namespace>

# 멀티 컨테이너 Pod인 경우 컨테이너 지정
kubectl logs <pod-name> -c <container-name> --previous -n <namespace>

# 최근 50줄만 확인
kubectl logs <pod-name> --previous --tail=50 -n <namespace>
Tip
--previous 플래그는 직전 1회 종료된 컨테이너의 로그만 보여줍니다. Pod가 여러 번 재시작된 경우 이전 로그는 유실됩니다. 로그 수집 시스템(Fluentd, Fluent Bit, CloudWatch Logs 등)이 있다면 그쪽에서 전체 이력을 확인하는 것이 더 정확합니다.

2-3. Events 확인

kubectl describe pod <pod-name> -n <namespace> | grep -A 20 "Events"

Events에서 확인할 키워드:

이벤트 메시지 의미
Liveness probe failed Liveness Probe 응답 실패로 컨테이너 재시작
OOMKilling 메모리 limit 초과로 커널이 프로세스 종료
PostStartHook failed postStart lifecycle hook 실행 실패
Back-off restarting failed container CrashLoopBackOff 진입 확인
FailedMount Volume 마운트 실패
CrashLoopBackOff 진단 흐름
CrashLoopBackOff 진단 흐름

3. 원인별 분석

3-1. 코드 버그 / 미처리 예외

가장 흔한 원인입니다. 애플리케이션이 시작 직후 예외를 던지거나 패닉 상태로 종료하는 경우입니다.

Exit Code: 1 (대부분)

로그 예시:

# Java
Exception in thread "main" java.lang.NullPointerException
    at com.myapp.Application.main(Application.java:25)

# Python
Traceback (most recent call last):
  File "/app/main.py", line 10, in <module>
    from mymodule import missing_function
ImportError: cannot import name 'missing_function'

# Node.js
TypeError: Cannot read properties of undefined (reading 'port')
    at Object.<anonymous> (/app/server.js:12:25)

실무 시나리오:

CI/CD에서 새 버전을 배포했는데, 코드에서 사용하는 라이브러리 버전이 바뀌면서 API 호환성이 깨졌습니다. 로컬과 CI 테스트에서는 잘 되지만 클러스터에서는 다른 환경변수 조합으로 실행되어 특정 코드 경로에서 예외가 발생합니다.

확인 방법:

# 이전 컨테이너 로그에서 에러 확인
kubectl logs <pod-name> --previous -n <namespace>

# 에러 발생 시점 확인 (시작 후 즉시 종료인지, 몇 초 후인지)
kubectl describe pod <pod-name> -n <namespace> | grep -A 5 "Last State"

해결:

  • 로그의 에러 메시지를 기반으로 코드 수정
  • 이전 버전으로 롤백 후 원인 분석
# 이전 버전으로 롤백
kubectl rollout undo deployment/<deployment-name> -n <namespace>

# 롤백 상태 확인
kubectl rollout status deployment/<deployment-name> -n <namespace>

3-2. 환경변수 / 설정 누락

애플리케이션이 시작 시 필요한 환경변수나 설정 파일이 없어서 종료하는 경우입니다. ConfigMap이나 Secret이 존재하지 않거나, 키 이름이 다른 경우에 발생합니다.

Exit Code: 1

로그 예시:

Error: DATABASE_URL environment variable is not set
Error: Failed to load config file: /etc/myapp/config.yaml: no such file or directory
panic: required key DB_PASSWORD missing value

실무 시나리오:

개발 환경에서는 .env 파일로 환경변수를 관리합니다. 스테이징 클러스터에 배포할 때 ConfigMap에 DATABASE_HOST는 넣었지만 DATABASE_PORT를 빠뜨렸습니다. 애플리케이션은 시작 시 모든 필수 환경변수를 체크하고, 하나라도 없으면 Exit 1로 종료합니다.

확인 방법:

# Pod에 주입된 환경변수 확인
kubectl exec <pod-name> -n <namespace> -- env
# CrashLoopBackOff 상태에서는 exec이 안 될 수 있음

# 대안: Pod spec에서 env 설정 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].env}' | jq .

# ConfigMap 존재 여부 확인
kubectl get configmap <configmap-name> -n <namespace>

# Secret 존재 여부 확인
kubectl get secret <secret-name> -n <namespace>

# ConfigMap/Secret의 키 목록 확인
kubectl get configmap <configmap-name> -n <namespace> -o jsonpath='{.data}' | jq 'keys'

해결:

# ConfigMap에 누락된 키 추가
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
  namespace: production
data:
  DATABASE_HOST: "db.internal.svc.cluster.local"
  DATABASE_PORT: "5432"       # 누락된 키 추가
  DATABASE_NAME: "myapp"
# ConfigMap 업데이트 후 Pod 재시작
kubectl apply -f configmap.yaml
kubectl rollout restart deployment/<deployment-name> -n <namespace>
주의
ConfigMap이나 Secret을 업데이트해도 기존 Pod에는 자동 반영되지 않습니다(envFrom 사용 시). kubectl rollout restart로 Pod를 재생성해야 새 값이 적용됩니다. Volume으로 마운트한 경우에는 kubelet이 주기적으로 갱신하지만, 갱신 간격(기본 1분)이 있으므로 즉시 반영이 필요하면 재시작이 확실합니다.

3-3. OOMKilled (메모리 limit 초과)

컨테이너의 메모리 사용량이 resources.limits.memory를 초과하면 Linux 커널의 OOM Killer가 프로세스를 강제 종료합니다.

Exit Code: 137 (128 + 9 = SIGKILL)

Events 메시지:

Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

실무 시나리오:

Java 애플리케이션의 메모리 limit을 512Mi로 설정했습니다. JVM의 -Xmx는 설정하지 않았습니다. JVM은 컨테이너의 메모리 제한을 인식하고 기본 힙을 제한의 25%(128Mi)로 잡지만, Metaspace, Thread Stack, Native Memory를 합치면 512Mi를 초과하여 OOMKilled가 발생합니다. 시작 직후에는 괜찮다가, 트래픽이 들어오면서 힙 사용량이 증가하여 종료됩니다.

확인 방법:

# 1. OOMKilled 여부 확인
kubectl describe pod <pod-name> -n <namespace> | grep -A 3 "Last State"

# 2. 현재 메모리 사용량 확인 (metrics-server 필요)
kubectl top pod <pod-name> -n <namespace>

# 3. Node 수준에서 OOM 이벤트 확인
kubectl describe node <node-name> | grep -i oom

# 4. 설정된 limit 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].resources}'

해결:

spec:
  containers:
    - name: my-app
      resources:
        requests:
          memory: "512Mi"    # 평상시 사용량 기준
        limits:
          memory: "1Gi"      # 피크 사용량 + 여유분

메모리 설정 가이드라인:

항목 권장 방법
requests 평상시 메모리 사용량 (P50)
limits 피크 사용량의 1.5~2배 또는 P99
JVM 앱 -Xmx를 limit의 70~75%로 설정 (나머지는 Metaspace, Stack용)
Node.js --max-old-space-size를 limit의 75%로 설정
측정 부하 테스트 후 kubectl top pod로 실제 사용량 확인
Tip
OOMKilled가 반복되면 무조건 limit을 올리기보다, 먼저 메모리 프로파일링으로 누수 여부를 확인하는 것이 좋습니다. 누수가 있으면 limit을 아무리 올려도 시간이 지나면 다시 OOMKilled가 발생합니다.

3-4. Command / Entrypoint 오류

Pod spec의 commandargs가 잘못되었거나, Dockerfile의 ENTRYPOINT/CMD가 올바르지 않은 경우입니다.

Exit Code: 126 (실행 권한 없음) 또는 127 (명령어 없음)

로그 예시:

# Exit 127
exec /app/start.sh: no such file or directory

# Exit 126
exec /app/start.sh: permission denied

# Shell이 없는 scratch 이미지에서 shell 명령 실행 시도
exec: "/bin/sh": stat /bin/sh: no such file or directory

실무 시나리오:

Dockerfile에서 multi-stage build를 사용하여 최종 이미지를 scratch 또는 distroless 기반으로 만들었습니다. Pod spec에서 command: ["/bin/sh", "-c", "..."]를 사용했는데, 해당 이미지에는 shell이 없어서 127로 종료됩니다.

확인 방법:

# Pod spec의 command/args 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].command}'
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].args}'

# 이미지의 ENTRYPOINT/CMD 확인 (로컬에서)
docker inspect <image>:<tag> --format='{{.Config.Entrypoint}} {{.Config.Cmd}}'

해결:

# 1. command가 이미지 내 실제 바이너리 경로를 가리키는지 확인
spec:
  containers:
    - name: my-app
      image: my-app:v1.2.3
      command: ["/app/my-app"]         # 바이너리 직접 실행
      # command: ["/bin/sh", "-c"]     # scratch 이미지에서는 사용 불가

# 2. 실행 권한 문제라면 Dockerfile에서 수정
# Dockerfile
RUN chmod +x /app/start.sh

3-5. Liveness Probe 실패

Liveness Probe가 실패하면 kubelet이 컨테이너를 강제 재시작합니다. 앱 시작 시간보다 initialDelaySeconds가 짧으면, 앱이 준비되기 전에 Probe가 실패하여 무한 재시작에 빠집니다.

Events 메시지:

Warning  Unhealthy  5s (x3 over 15s)  kubelet  Liveness probe failed: 
  Get "http://10.0.1.5:8080/health": dial tcp 10.0.1.5:8080: connect: connection refused
Warning  Unhealthy  5s                kubelet  Liveness probe failed: HTTP probe failed with statuscode: 503

실무 시나리오:

Spring Boot 애플리케이션의 시작 시간이 30초입니다. Liveness Probe의 initialDelaySeconds를 10초로 설정했습니다. 앱이 아직 시작 중인데 Probe가 먼저 실행되어 실패합니다. 3회 연속 실패(failureThreshold 기본값) 후 컨테이너가 재시작되고, 다시 30초 걸려서 시작하는데 또 10초 만에 Probe가 실행됩니다. 무한 루프입니다.

확인 방법:

# 1. Probe 설정 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].livenessProbe}' | jq .

# 2. 앱 실제 시작 시간 확인 (로그에서)
kubectl logs <pod-name> --previous -n <namespace> | grep -i "started\|ready\|listening"

# 3. Events에서 Probe 실패 확인
kubectl describe pod <pod-name> -n <namespace> | grep -i "unhealthy\|probe"

해결 — Startup Probe 사용 (권장):

Kubernetes 1.20+ 에서는 startupProbe를 사용하여 시작 시간이 긴 앱을 안전하게 처리할 수 있습니다. startupProbe가 성공할 때까지 livenessProbe와 readinessProbe는 비활성화됩니다.

spec:
  containers:
    - name: my-app
      livenessProbe:
        httpGet:
          path: /health
          port: 8080
        periodSeconds: 10
        failureThreshold: 3
      startupProbe:
        httpGet:
          path: /health
          port: 8080
        periodSeconds: 5
        failureThreshold: 30    # 5초 × 30회 = 최대 150초 대기

해결 — initialDelaySeconds 조정 (startupProbe 미사용 시):

spec:
  containers:
    - name: my-app
      livenessProbe:
        httpGet:
          path: /health
          port: 8080
        initialDelaySeconds: 60   # 앱 시작 시간보다 충분히 길게
        periodSeconds: 10
        failureThreshold: 3
        timeoutSeconds: 5

Probe 설정 설계 기준:

파라미터 권장값 산정 방법
initialDelaySeconds 앱 시작 시간 P95 + 여유 10초 (startupProbe 없을 때)
periodSeconds 10~30초 (너무 짧으면 불필요한 부하)
failureThreshold 3 (기본값, 일시적 실패 허용)
timeoutSeconds 앱 health endpoint 응답 P99 + 1~2초
주의: Liveness vs Readiness 혼동
Liveness Probe 실패 → 컨테이너 재시작 (강제 종료). Readiness Probe 실패 → Service에서 트래픽만 제외 (컨테이너 유지). 시작 시간이 긴 앱에서 Liveness Probe를 너무 공격적으로 설정하면 무한 재시작 루프에 빠집니다. 시작 완료 전까지는 startupProbe로 보호하고, 이후에만 livenessProbe를 동작시키는 것이 안전합니다.

3-6. 의존 서비스 연결 실패

애플리케이션이 시작 시 데이터베이스, Redis, 외부 API 등에 연결을 시도하는데, 해당 서비스가 아직 준비되지 않았거나 접근이 차단된 경우입니다.

Exit Code: 1

로그 예시:

Error: connect ECONNREFUSED 10.100.50.30:5432
Failed to connect to Redis at redis.default.svc.cluster.local:6379: Connection refused
panic: dial tcp 10.100.50.30:3306: i/o timeout

실무 시나리오:

마이크로서비스 환경에서 여러 Deployment를 동시에 배포합니다. App 서비스가 DB 서비스보다 먼저 시작되면 연결에 실패합니다. 또는 NetworkPolicy가 적용된 클러스터에서 새 서비스를 추가했는데, 해당 서비스에 대한 Ingress 규칙을 빠뜨린 경우에도 동일한 증상이 나타납니다.

확인 방법:

# 1. 의존 서비스 상태 확인
kubectl get pods -n <namespace> | grep <dependency-name>
kubectl get svc -n <namespace> | grep <dependency-name>

# 2. Pod에서 의존 서비스로 연결 테스트 (디버그 컨테이너 사용)
kubectl debug -it <pod-name> --image=busybox -- sh
# 또는 임시 Pod로 테스트
kubectl run test-net --rm -it --image=busybox -- sh
nslookup <service-name>.<namespace>.svc.cluster.local
nc -zv <service-name>.<namespace>.svc.cluster.local <port>

# 3. NetworkPolicy 확인
kubectl get networkpolicy -n <namespace>
kubectl describe networkpolicy <policy-name> -n <namespace>

해결:

  1. initContainer로 의존 서비스 대기:
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command: ['sh', '-c', 'until nc -z db.default.svc.cluster.local 5432; do echo waiting for db; sleep 2; done']
  containers:
    - name: my-app
      image: my-app:v1.2.3
  1. 앱 레벨에서 재시도 로직 구현:

연결 실패 시 즉시 종료하지 않고, 지수 백오프로 재시도하는 것이 운영 환경에서는 더 안정적입니다. initContainer 방식은 시작 시에만 체크하므로, 시작 후 의존 서비스가 잠시 다운되면 대응할 수 없습니다.

  1. NetworkPolicy 규칙 추가:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-db
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: my-db
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: my-app
      ports:
        - port: 5432
          protocol: TCP

3-7. Volume 마운트 권한 문제

컨테이너가 Volume에 쓰기를 시도하는데 권한이 없어서 실패하는 경우입니다. 특히 non-root 사용자로 실행하는 컨테이너에서 자주 발생합니다.

Exit Code: 1

로그 예시:

Error: EACCES: permission denied, open '/data/logs/app.log'
mkdir: cannot create directory '/data/cache': Permission denied

실무 시나리오:

보안을 위해 Dockerfile에서 USER 1000으로 non-root 실행을 설정했습니다. PersistentVolume을 /data에 마운트했는데, 해당 볼륨의 소유자가 root여서 UID 1000인 프로세스가 쓰기를 할 수 없습니다.

확인 방법:

# 1. 컨테이너 실행 사용자 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[0].securityContext}'

# 2. Pod 수준 securityContext 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.securityContext}'

해결:

spec:
  securityContext:
    fsGroup: 1000          # 마운트된 볼륨의 그룹을 1000으로 변경
    runAsUser: 1000
    runAsGroup: 1000
  containers:
    - name: my-app
      image: my-app:v1.2.3
      volumeMounts:
        - name: data
          mountPath: /data
Tip
fsGroup을 설정하면 마운트된 볼륨의 모든 파일에 해당 GID가 소유 그룹으로 적용됩니다. 다만 대용량 볼륨에서는 마운트 시 모든 파일의 권한을 변경하느라 시작이 느려질 수 있습니다. Kubernetes 1.23+에서는 fsGroupChangePolicy: OnRootMismatch를 사용하면 루트 디렉토리 권한이 다를 때만 변경하여 시작 시간을 단축할 수 있습니다.

3-8. 프로세스가 포그라운드에서 실행되지 않음

컨테이너의 메인 프로세스(PID 1)가 백그라운드로 전환되면 즉시 종료된 것으로 인식됩니다. Kubernetes는 PID 1 프로세스가 살아있는 동안만 컨테이너가 실행 중이라고 판단합니다.

Exit Code: 0

실무 시나리오:

Nginx를 컨테이너로 실행하는데, 설정에 daemon off; 를 넣지 않았습니다. Nginx가 시작되면 마스터 프로세스가 백그라운드로 전환(daemonize)하면서 포그라운드 프로세스가 종료됩니다. Exit 0이지만 restartPolicy: Always에 의해 계속 재시작됩니다.

해결:

# Nginx 예시 — 포그라운드 실행
CMD ["nginx", "-g", "daemon off;"]

# 일반적인 셸 스크립트 — exec으로 PID 1 전달
#!/bin/sh
exec java -jar /app/my-app.jar
# exec 없이 실행하면 sh가 PID 1이 되고, java는 자식 프로세스
소프트웨어 포그라운드 실행 방법
Nginx nginx -g "daemon off;"
Apache httpd -DFOREGROUND
Redis 기본이 포그라운드 (설정에서 daemonize no)
MySQL mysqld (기본 포그라운드)
커스텀 스크립트 exec 으로 메인 프로세스를 PID 1로 실행

4. 원인 구조 한눈에 보기

CrashLoopBackOff 원인 분류
CrashLoopBackOff 원인 분류

5. 실무 시나리오: 배포 직후 CrashLoopBackOff 디버깅

상황: CI/CD로 새 버전(v2.1.0)을 배포했는데, 새 Pod가 CrashLoopBackOff에 빠집니다. 이전 버전(v2.0.0) Pod는 정상 동작 중입니다.

진단 과정:

# 1. 상태 확인
kubectl get pods -n production | grep my-app
# my-app-v2-abc123  0/1  CrashLoopBackOff  5  3m
# my-app-v1-def456  1/1  Running           0  2d

# 2. Exit Code 확인
kubectl describe pod my-app-v2-abc123 -n production | grep -A 5 "Last State"
# Last State:  Terminated
#   Reason:    Error
#   Exit Code: 1

# 3. 이전 로그 확인
kubectl logs my-app-v2-abc123 --previous -n production
# 2026-06-05T10:30:01Z ERROR Failed to initialize cache connection
# 2026-06-05T10:30:01Z ERROR Required environment variable REDIS_URL is not set
# 2026-06-05T10:30:01Z FATAL Application startup failed, exiting

# 4. 원인 식별: 환경변수 누락
# v2.1.0에서 새로 추가된 Redis 캐시 기능에 REDIS_URL이 필요한데
# ConfigMap에 아직 추가되지 않았음

# 5. 해결
kubectl edit configmap my-app-config -n production
# REDIS_URL: "redis://redis.production.svc.cluster.local:6379" 추가

kubectl rollout restart deployment/my-app -n production
kubectl rollout status deployment/my-app -n production
# deployment "my-app" successfully rolled out

사후 분석:

  • CI/CD 파이프라인에서 새 환경변수가 추가되면 ConfigMap 업데이트를 포함시키는 프로세스가 필요합니다.
  • 앱에서 필수 환경변수 체크 시 명확한 에러 메시지를 출력하도록 구현하면 디버깅 시간이 단축됩니다.
  • Helm values 또는 Kustomize overlay에서 환경별 설정을 관리하면 누락 위험을 줄일 수 있습니다.

6. 디버깅 도구와 기법

6-1. Ephemeral Debug Container

CrashLoopBackOff 상태에서는 kubectl exec이 동작하지 않습니다(컨테이너가 실행 중이지 않으므로). Kubernetes 1.25+에서는 Ephemeral Container를 사용하여 실행 중이지 않은 Pod에도 디버그 컨테이너를 붙일 수 있습니다.

# 디버그 컨테이너 추가
kubectl debug -it <pod-name> --image=busybox --target=<container-name> -n <namespace>

# 또는 Node 수준에서 디버깅
kubectl debug node/<node-name> -it --image=ubuntu

6-2. Command 오버라이드로 진입

컨테이너가 시작 즉시 종료되어 로그도 확인이 어려운 경우, command를 sleep으로 오버라이드하여 컨테이너를 살려둔 상태에서 내부를 조사할 수 있습니다.

# Deployment의 command를 일시적으로 오버라이드
kubectl patch deployment <name> -n <namespace> --type='json' \
  -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/command", "value": ["sleep", "3600"]}]'

# 컨테이너 접속 후 수동으로 앱 실행하여 에러 확인
kubectl exec -it <pod-name> -n <namespace> -- sh
/app/start.sh    # 직접 실행하여 에러 확인
주의
이 방법은 디버깅 전용입니다. 조사가 끝나면 반드시 원래 command로 복원하세요. 프로덕션에서는 별도 디버깅용 Pod를 생성하는 것이 안전합니다.

6-3. 유용한 디버깅 명령어 모음

# Pod 전체 상태 YAML로 확인
kubectl get pod <pod-name> -n <namespace> -o yaml

# 재시작 횟수와 마지막 종료 시점 확인
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[0].restartCount}'
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.status.containerStatuses[0].lastState}'

# 특정 네임스페이스에서 CrashLoopBackOff인 Pod만 필터링
kubectl get pods -n <namespace> --field-selector=status.phase!=Running | grep CrashLoopBackOff

# 클러스터 전체에서 CrashLoopBackOff Pod 찾기
kubectl get pods --all-namespaces | grep CrashLoopBackOff

# Events를 시간순으로 정렬하여 확인
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep <pod-name>

7. 재발 방지 체크리스트

항목 권장 설정 이유
Startup Probe 시작 시간이 긴 앱에 설정 Liveness Probe에 의한 무한 재시작 방지
메모리 requests/limits 부하 테스트 후 실측값 기반 설정 OOMKilled 방지, 추측 기반 설정 위험
필수 환경변수 검증 앱 시작 시 누락 체크 + 명확한 에러 메시지 디버깅 시간 단축
Readiness Probe 의존 서비스 포함 체크 준비되지 않은 Pod로 트래픽 유입 방지
initContainer 필수 의존 서비스 대기 의존 서비스 미준비로 인한 반복 실패 방지
CI/CD 환경 검증 배포 전 ConfigMap/Secret 존재 확인 설정 누락으로 인한 배포 실패 방지
이미지 빌드 포그라운드 실행, 적절한 ENTRYPOINT Exit 0 무한 재시작 방지
롤백 전략 maxUnavailable, maxSurge 설정 실패 시 빠른 자동 롤백

8. 정리

  • CrashLoopBackOff는 "컨테이너가 반복 종료 후 재시작 대기 중"이라는 상태입니다. 원인은 Exit Code와 로그에서 확인합니다.
  • Exit Code 1(일반 에러)이 가장 흔하고, 로그의 에러 메시지가 직접적인 원인을 알려줍니다. Exit Code 137은 OOMKilled를 의미합니다.
  • Liveness Probe 설정 오류로 인한 무한 재시작은 자주 발생하는 실수입니다. startupProbe를 사용하면 시작 시간이 긴 앱도 안전하게 보호할 수 있습니다.
  • 새 버전 배포 후 발생하면 로그 확인 → 롤백 → 원인 분석 순서로 대응합니다. 운영 환경에서는 서비스 복구가 디버깅보다 우선입니다.
  • 의존 서비스 연결 실패는 initContainer나 앱 레벨 재시도 로직으로 방어합니다. 둘 다 적용하면 더 안정적입니다.

참고 문서

반응형