/인프라/CrashLoopBackOff 해결: kubectl로 Pod 재시작 무한루프 5분 진단
인프라CrashLoopBackOffkubernetes-troubleshooting

CrashLoopBackOff 해결: kubectl로 Pod 재시작 무한루프 5분 진단

Pod가 CrashLoopBackOff로 무한 재시작될 때 kubectl describe와 logs --previous로 5분 안에 원인 찾기. Exit Code 1/2/126/137 진단표와 command·probe·ConfigMap 원인별 복붙 처방 YAML 정리.

CrashLoopBackOff 해결: kubectl로 Pod 재시작 무한루프 5분 진단

CrashLoopBackOff 완벽 해결: kubectl로 Pod 재시작 무한루프 5분 진단

K8s Troubleshooting Guide 18편

"Back-off restarting failed container" — 지금 이 화면을 보고 있다면

배포는 끝났는데 Pod가 떴다 죽기를 반복하고, RESTARTS 카운트만 계속 올라가는 상황. 십중팔구 아래 화면을 보고 있을 겁니다.

Bash
$ kubectl get pods
NAME                      READY   STATUS             RESTARTS      AGE
api-server-7d9f8c-x2kpq   0/1     CrashLoopBackOff   6 (90s ago)   8m

STATUSCrashLoopBackOffRESTARTS가 계속 증가하면, 쿠버네티스는 "이 컨테이너 또 죽었네, 잠깐 기다렸다 다시 띄울게"를 반복 중입니다. 재시작 간격은 10초 → 20초 → 40초… 최대 5분까지 지수적으로 늘어납니다(back-off).

헷갈리는 형제 에러부터 한 줄로 정리하고 갑시다.

  • ImagePullBackOff: 컨테이너가 아예 안 뜸. 이미지를 못 가져옴(레지스트리/태그/인증 문제).
  • OOMKilled: 메모리 한도 초과로 커널이 죽임(Exit 137). → CrashLoop의 한 원인이기도 함.
  • CrashLoopBackOff: 컨테이너는 떴다가 죽기를 반복. ← 이 글이 정조준하는 케이스.

즉 "이미지는 잘 받았는데 프로세스가 시작 직후 또는 잠시 후 종료된다"가 핵심입니다.

1차 진단 3종 세트: describe · logs --previous · Exit Code

원인을 추측하지 말고 명령어로 좁힙니다. 아래 세 개만 순서대로 치세요.

① describe로 Events와 Last State 확인

Bash
kubectl describe pod api-server-7d9f8c-x2kpq

출력에서 볼 핵심 구간:

TEXT
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Wed, 25 Jun 2026 10:11:02 +0900
      Finished:     Wed, 25 Jun 2026 10:11:03 +0900
...
Events:
  Type     Reason     Age                 From     Message
  ----     ------     ----                ----     -------
  Warning  BackOff    20s (x6 over 7m)    kubelet  Back-off restarting failed container

Last State: TerminatedExit CodeReason이 1차 단서입니다. StartedFinished 시각 차이를 보면 "1초 만에 죽었는지", "한참 돌다 죽었는지"도 알 수 있습니다.

② 죽기 직전 로그 보기 (가장 중요)

현재 컨테이너는 죽고 새로 떠서 kubectl logs가 비어 있을 때가 많습니다. 직전(죽은) 컨테이너 로그를 봐야 합니다.

Bash
kubectl logs api-server-7d9f8c-x2kpq --previous

--previous(또는 -p)가 CrashLoop 진단의 핵심입니다. 대부분의 진짜 원인(스택트레이스, "config not found", "connection refused")이 여기 찍혀 있습니다.

③ Exit Code를 정확히 뽑기

Bash
kubectl get pod api-server-7d9f8c-x2kpq \
  -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'

# 이벤트만 시간순으로
kubectl get events --field-selector involvedObject.name=api-server-7d9f8c-x2kpq \
  --sort-by=.lastTimestamp

Exit Code 진단표

Exit Code의미추정 원인다음 확인 액션
0정상 종료인데 재시작메인 프로세스가 할 일 끝나고 종료(배치성), restartPolicy 부적합Job/CronJob으로 바꾸거나 foreground 프로세스 유지 확인
1앱 일반 오류코드 예외, 환경변수/Config 누락, DB 연결 실패logs --previous로 스택트레이스, env/ConfigMap 점검
2셸/인자 오류잘못된 옵션, 셸 스크립트 문법 오류entrypoint 스크립트, command/args 검토
126실행 권한 없음바이너리에 실행 비트 없음, 스크립트 chmod 누락이미지 내 ls -l, Dockerfile RUN chmod +x
127명령어 없음command 오타, 바이너리 미설치, PATH 문제command/args 경로 확인, 베이스 이미지 점검
137SIGKILL (128+9)OOMKilled 또는 강제 종료describe의 Reason: OOMKilled, 메모리 limit 점검
143SIGTERM (128+15)정상 종료 신호 받음(롤링/preStop)graceful shutdown 처리, probe 타이밍 점검

원인별 복붙 처방전 5가지

1) 잘못된 command/args (Exit 127/126)

증상: logs --previousexec: "start.sh": not found 또는 permission denied.

확인:

Bash
kubectl get pod <pod> -o jsonpath='{.spec.containers[0].command}'

수정 YAML (경로·권한 교정):

YAML
spec:
  containers:
    - name: api
      image: myregistry/api:1.4.0
      command: ["/app/bin/server"]   # 절대경로, 실제 존재하는 바이너리
      args: ["--port=8080"]

스크립트라면 이미지 빌드 시 RUN chmod +x /app/bin/server를 잊지 마세요(126 방지).

2) 의존 서비스 미준비 (initContainer로 대기)

증상: 앱이 DB/Redis에 붙으려다 connection refused로 Exit 1, 의존 서비스가 늦게 뜨면 무한 재시작.

확인:

Bash
kubectl logs <pod> --previous | grep -i "refused\|timeout\|unreachable"

수정 YAML (wait-for 패턴):

YAML
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          until nc -z postgres 5432; do
            echo "waiting for postgres..."; sleep 2;
          done
  containers:
    - name: api
      image: myregistry/api:1.4.0

3) 환경변수·ConfigMap/Secret 누락 (Exit 1)

GitOps 환경에서 ConfigMap만 빠진 채 Deployment가 먼저 동기화되면 단골로 터지는 케이스입니다.

증상: Error: configmap "app-config" not found 또는 앱 로그에 KeyError: DATABASE_URL.

확인:

Bash
kubectl get configmap app-config
kubectl describe pod <pod> | grep -A5 "Environment"

수정 YAML:

YAML
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secret

마운트 키 이름이 앱이 읽는 환경변수명과 정확히 일치하는지 꼭 대조하세요.

4) liveness probe 과민 설정

증상: 앱은 멀쩡한데 부팅이 느려서 liveness가 먼저 죽임 → 무한 재시작. (다음 섹션에서 정석 해결)

5) OOM 연계 (Exit 137)

증상: describeReason: OOMKilled, Exit 137. 확인: kubectl describe pod <pod> → Last State Reason. 메모리 limits를 올리거나 앱 힙을 조정해야 합니다. 자세한 내용은 OOMKilled 편을 참고하세요.

probe 튜닝으로 루프 끊기: startupProbe 실전

가장 억울한 CrashLoop은 "앱은 잘 떴는데 liveness에 학살당하는" 패턴입니다. JVM·대형 모델 로딩처럼 부팅이 느린 앱은 initialDelaySeconds만으로 버티기 어렵습니다. 정답은 startupProbe로 부팅 구간을 분리하는 것입니다.

YAML
    livenessProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      failureThreshold: 3        # 부팅과 무관하게 짧게 유지
    readinessProbe:
      httpGet: { path: /ready, port: 8080 }
      periodSeconds: 5
    startupProbe:
      httpGet: { path: /healthz, port: 8080 }
      periodSeconds: 10
      failureThreshold: 30       # 10s * 30 = 최대 300초까지 부팅 허용

startupProbe가 성공할 때까지 liveness/readiness는 잠잠합니다. 부팅이 끝나면 그때부터 liveness가 동작하니, "느린 부팅"과 "운영 중 장애"를 깔끔히 분리할 수 있습니다.

probe 권장값 표

필드livenessreadinessstartupProbe
initialDelaySeconds0 (startup이 있으면 불필요)50
periodSeconds10510
failureThreshold3330 (부팅 시간/period)
timeoutSeconds1~21~22~3

실무 경험 한마디: Kubernetes 1.30+에서 sidecar가 native initContainer(restartPolicy: Always)로 정식화되면서, 로그 수집·프록시 사이드카가 메인보다 늦게 떠 발생하던 CrashLoop이 많이 줄었습니다. 다만 GitOps(Argo CD 등)에서 ConfigMap sync가 누락되어 터지는 CrashLoop은 여전히 1위입니다. 저는 배포 후 무조건 logs --previous부터 보고, 그 다음 Exit Code를 확인하는 습관을 추천합니다. 추측보다 출력이 빠릅니다.

결론: 재발 방지 체크리스트

루프를 끊었다면 같은 일이 또 일어나지 않게 못을 박읍시다.

  • 로컬에서 컨테이너 단독 실행 검증: docker run --rm <image>로 K8s 밖에서 먼저 떠보기
  • ConfigMap/Secret 마운트 키 이름이 앱이 읽는 키와 1:1로 일치하는지 대조
  • probe 분리 원칙: 부팅은 startupProbe, 운영 헬스는 liveness, 트래픽 수신은 readiness
  • 의존성: DB/캐시는 initContainer wait-for 또는 readinessProbe로 보호
  • RESTARTS 모니터링/알람: kube_pod_container_status_restarts_total 메트릭에 알람 설정
  • ImagePullBackOff 편 — 이미지를 아예 못 가져올 때
  • OOMKilled 편 — Exit 137 메모리 초과 정밀 진단
  • PVC Pending 편 — 볼륨 바인딩 실패로 Pod가 안 뜰 때

다음 편 예고

19편에서는 Pod Pending & 스케줄링 실패(노드 리소스 부족, taint/toleration, nodeSelector 미스매치)를 kubectl describeFailedScheduling 이벤트부터 추적합니다.

참고: 공식 문서

이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.

자주 묻는 질문 (FAQ)

Q. kubectl logs가 비어 있는데 어떻게 죽은 이유를 보나요? A. 현재 컨테이너는 막 새로 떠서 로그가 없을 수 있습니다. kubectl logs <pod> --previous(-p)로 직전에 죽은 컨테이너의 로그를 보세요. 대부분의 원인이 여기에 찍힙니다.

Q. Exit Code 137이면 무조건 메모리 부족인가요? A. 137은 SIGKILL(128+9)을 받았다는 뜻입니다. 대부분 OOMKilled지만, 노드 압박이나 강제 종료로도 발생합니다. kubectl describe podLast State → Reason: OOMKilled 여부로 메모리 문제인지 확정하세요.

Q. 앱은 정상인데 계속 재시작돼요. 무엇부터 볼까요? A. liveness probe 과민 설정일 확률이 높습니다. 부팅 시간을 startupProbe로 분리하고 liveness의 failureThreshold는 짧게 유지하세요. 부팅이 끝나기 전 liveness가 동작하면 멀쩡한 앱도 무한 재시작됩니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

초안 · AI (Content Reviewer)·검토 · Nodelog 편집자·발행 ·
관련 공식 문서Kubernetes 공식 문서

댓글

첫 번째 댓글을 남겨보세요.