Skip to content
Published on

Kubernetes CrashLoopBackOff の原因別診断と解決 — ログが空のとき何を見ればよいか

シェア
Authors

はじめに — RESTARTS の数字だけが増えてログは空です

デプロイ直後に Pod 一覧を見ると、こんな様子です。

kubectl get pod -n payments

NAME                        READY   STATUS             RESTARTS        AGE
checkout-7d9f6c5b4d-2xk9p   0/1     CrashLoopBackOff   6 (2m41s ago)   11m
checkout-7d9f6c5b4d-lm4vz   0/1     CrashLoopBackOff   6 (2m38s ago)   11m
checkout-7d9f6c5b4d-q8w2n   0/1     CrashLoopBackOff   6 (2m44s ago)   11m

ログを見ようとしても何も出てきません。

kubectl logs checkout-7d9f6c5b4d-2xk9p -n payments

Error from server (BadRequest): container "checkout" in pod "checkout-7d9f6c5b4d-2xk9p" is waiting to start: CrashLoopBackOff

ここで多くの人が「ログを出さないアプリだ」と結論づけ、ロギング設定を探し始めます。それは誤った方向です。ログは残っています。いま照会しようとしているコンテナが、まだ起動していない次の世代のコンテナであるだけです。

CrashLoopBackOff は原因ではなく再起動待ちです

名前を正確に読む必要があります。CrashLoopBackOff はコンテナがなぜ死んだのかについて何も教えてくれません。kubelet が「このコンテナは死に続けるので、再起動をしばらく先送りする」と宣言した状態にすぎません。

kubelet の再起動遅延は決まった規則に従います。

  • 最初の失敗後は 10 秒待機
  • 失敗するたびに待機時間が 2 倍に増加 — 10 秒、20 秒、40 秒、80 秒、160 秒
  • 上限は 300 秒、つまり 5 分
  • コンテナが十分に長く(既定では 10 分)正常稼働すると遅延タイマーがリセットされ、再び 10 秒から始まる

この規則から二つの実務的な結論が出てきます。

第一に、Pod が長く放置されていると再起動間隔は 5 分まで広がっています。修正したイメージを上げて「なぜまだ立ち上がらないのか」と待つ必要はありません。Pod を削除すれば、新しい Pod はバックオフの履歴なしに即座に起動します。

kubectl delete pod checkout-7d9f6c5b4d-2xk9p -n payments

第二に、RESTARTS カウントが少しずつ増える Pod は CrashLoopBackOff として表示されないこともあります。10 分以上持ちこたえてから死ぬアプリは毎回バックオフがリセットされるため、ステータス列には Running と見えて RESTARTS だけが静かに増えていきます。このパターンのほうがむしろ危険です。以下のコマンドで定期的に洗い出す必要があります。

kubectl get pods -A --sort-by='.status.containerStatuses[0].restartCount' | tail -20

NAMESPACE   NAME                          READY   STATUS    RESTARTS         AGE
search      indexer-6c4d9f7b8-h2klp       1/1     Running   17 (43m ago)     6d
payments    ledger-5f8b7c6d9-nm3xt        1/1     Running   23 (12m ago)     9d

診断の出発点 — Last State、Exit Code、そして previous ログ

診断の順序はいつも同じです。describe で死んだ理由を確認し、その次に死んだコンテナのログを読みます。

kubectl describe pod checkout-7d9f6c5b4d-2xk9p -n payments

Containers:
  checkout:
    Container ID:   containerd://3f2a91c4e88b7d5641a0c2f9e37b1d80
    Image:          registry.example.com/checkout:1.14.2
    Image ID:       registry.example.com/checkout@sha256:9c1f...
    Port:           8080/TCP
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Sun, 26 Jul 2026 09:41:12 +0900
      Finished:     Sun, 26 Jul 2026 09:41:13 +0900
    Ready:          False
    Restart Count:  6
Events:
  Type     Reason     Age                    From     Message
  ----     ------     ----                   ----     -------
  Normal   Pulled     3m12s (x5 over 11m)    kubelet  Container image "registry.example.com/checkout:1.14.2" already present on machine
  Normal   Created    3m12s (x5 over 11m)    kubelet  Created container checkout
  Normal   Started    3m11s (x5 over 11m)    kubelet  Started container checkout
  Warning  BackOff    2m41s (x24 over 10m)   kubelet  Back-off restarting failed container checkout

読むべき値は三つです。

  • Last State の Reason — Error、OOMKilled、Completed のどれか
  • Exit Code — 後述する終了コード辞典と照合する
  • Started と Finished の間隔 — 1 秒差なら起動中に即死、30 秒以上なら起動はしたがその後に死んだ

上の例は Started と Finished が 1 秒差です。プロセスが立ち上がった瞬間に死んだという意味なので、ランタイムのロジックではなく起動時点の設定を疑うべきです。

次に死んだコンテナのログを読みます。要点は --previous フラグです。kubelet はコンテナを再起動するときに新しいコンテナを作るため、フラグなしの kubectl logs はまだ起動していない、あるいは生まれたばかりのコンテナを指します。死んだコンテナのログは一つ前の世代にあります。

kubectl logs checkout-7d9f6c5b4d-2xk9p -n payments --previous

2026-07-26T00:41:12.881Z INFO  starting checkout 1.14.2
2026-07-26T00:41:13.104Z ERROR config: required key PAYMENT_SIGNING_KEY is not set
2026-07-26T00:41:13.105Z FATAL exiting with status 1

よくある誤答: ログが空だからアプリケーションがログを残さないと判断すること。実際には --previous を打っていないだけ、というケースが圧倒的に多いです。--previous まで空なら、そのときはじめてプロセスがログを一行も書けずに死んだという意味になり、その場合の原因はほぼ常にイメージのエントリポイントかボリュームマウントです。

コンテナが複数ある Pod なら、どのコンテナが死ぬのかを特定するところから始めます。

kubectl get pod checkout-7d9f6c5b4d-2xk9p -n payments \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'

checkout    6    Error    1
istio-proxy 0    <none>   <none>

終了コード辞典 — 0、1、127、137、139、143

終了コードは診断を半分に減らしてくれます。128 より大きい値は 128 にシグナル番号を足したものなので、どのシグナルで死んだかを逆算できます。

  • 0 — 正常終了。プロセスが自分の仕事を終えて完了したという意味です。Deployment の restartPolicy は常に Always なので、正常終了しても kubelet は再び起動し、結局 CrashLoopBackOff になります。Reason には Error ではなく Completed が出ます。原因はたいていプロセスをフォアグラウンドで実行していないことです。nginx をデーモンモードで起動する、シェルスクリプトが最後のコマンドをバックグラウンドに投げて終わる、といったケースが典型的です。
  • 1 — 一般的なアプリケーションエラー。フレームワークが捕まえられなかった例外、設定検証の失敗、必須環境変数の欠落がここに集まります。原因は必ずログにあります。
  • 2 — シェルの使い方エラー。誤った引数を渡したときにシェルが返します。
  • 126 — ファイルはあるが実行権限がない。エントリポイントスクリプトに実行ビットを付けていない場合です。
  • 127 — コマンドが見つからない。command にタイプミスがあるか、distroless または scratch イメージに存在しないシェルを呼び出した場合です。
  • 137 — 128 足す 9、つまり SIGKILL。カーネルの OOM キラーまたは kubelet が強制的に殺しました。Reason が OOMKilled ならメモリ上限の超過であり、Reason が Error なら SIGTERM 後の猶予時間内に終了できず強制終了されたということです。
  • 139 — 128 足す 11、つまり SIGSEGV。ネイティブコードのセグメンテーションフォルトです。ネイティブ拡張モジュール、JNI、CGO、あるいはアーキテクチャ不一致のイメージで出ます。
  • 143 — 128 足す 15、つまり SIGTERM。誰かが正常終了を要求し、プロセスが応じました。ローリングアップデートやノードのドレイン中なら正常ですが、理由もなく 143 が繰り返されるなら liveness プローブがコンテナを殺している可能性が高いです。

よくある誤答: 137 を見た瞬間にメモリ上限を上げること。137 は SIGKILL であるという事実だけを伝えます。メモリのせいかどうかは Reason フィールドが OOMKilled かどうかで判別しなければなりません。liveness 失敗後に終了を拒んで強制的に殺されたコンテナにメモリを足しても、何も変わりません。メモリ側で確定したなら OOMKilled 137 の診断 編に進むほうが早いです。

原因の分岐と診断コマンド

CrashLoopBackOff に収束する経路は大きく七つに分かれます。一つずつ排除していけばよいのです。

原因代表的な兆候診断コマンド解決
アプリケーション例外による即時終了Exit Code 1、Reason Error、実行 1 秒以内kubectl logs POD --previousログのスタックトレースをそのまま修正
必須設定・Secret の欠落Exit Code 1、ログにキー名が出るkubectl get secret, kubectl describe podConfigMap・Secret のキー名とネームスペースを確認
正常終了後に再起動を繰り返すExit Code 0、Reason Completedkubectl logs POD --previousプロセスをフォアグラウンドで実行
OOMKilledExit Code 137、Reason OOMKilleddescribe の Last State上限の引き上げまたはランタイムのヒープ設定調整
liveness プローブによる強制終了Exit Code 143 または 137、Events に Unhealthy と Killingkubectl get events --field-selector reason=UnhealthystartupProbe の追加、しきい値の緩和
誤った command・entrypointExit Code 126 または 127、Message に executable file not foundkubectl describe pod の Messagecommand と args の修正、イメージにシェルがあるか確認
ボリュームマウント失敗コンテナが起動すらできない、Events に FailedMountkubectl describe pod の EventsPVC のバインドと Secret の存在を確認
init コンテナの失敗STATUS が Init:CrashLoopBackOffkubectl logs POD -c INIT_NAME --previousinit ロジックの修正、依存サービスの起動順序を確認

設定と Secret の欠落

もっともよくあります。Pod スペックが参照するキーが実際に存在するかを確認するところから始めます。

kubectl get secret payment-keys -n payments -o jsonpath='{.data}' | tr ',' '\n'

{"PAYMENT_API_URL":"aHR0cHM6...","PAYMENT_WEBHOOK_SECRET":"czNjcjN0"}

スペックは PAYMENT_SIGNING_KEY を要求しているのに、Secret にはそのキーがありません。キー名のタイプミスが原因です。

ここに重要な設計上のポイントがあります。env の下に secretKeyRef で参照しながら optional: true を与えると、キーがなくても Pod は立ち上がり、アプリケーションがランタイムで死にます。逆に optional を明示しなければ、kubelet はコンテナを起動すらせず CreateContainerConfigError で止まります。後者のほうがはるかに診断しやすいです。

env:
  - name: PAYMENT_SIGNING_KEY
    valueFrom:
      secretKeyRef:
        name: payment-keys
        key: PAYMENT_SIGNING_KEY
        # optional を省略すると既定値は false — キーがなければコンテナを起動しない

このとき Pod のステータスは CrashLoopBackOff ではなく CreateContainerConfigError として現れ、describe に正確な理由が書かれます。

kubectl describe pod checkout-7d9f6c5b4d-2xk9p -n payments | grep -A3 "Warning  Failed"

  Warning  Failed     9s (x3 over 25s)  kubelet  Error: couldn't find key PAYMENT_SIGNING_KEY in Secret payments/payment-keys

liveness プローブが殺している場合

Events に Unhealthy と Killing が一緒に見えるなら、プローブが犯人です。

kubectl get events -n payments --field-selector involvedObject.name=checkout-7d9f6c5b4d-2xk9p --sort-by=.lastTimestamp

LAST SEEN   TYPE      REASON      OBJECT                          MESSAGE
4m12s       Warning   Unhealthy   pod/checkout-7d9f6c5b4d-2xk9p   Liveness probe failed: Get "http://10.42.3.17:8080/healthz": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
4m12s       Normal    Killing     pod/checkout-7d9f6c5b4d-2xk9p   Container checkout failed liveness probe, will be restarted

起動に 40 秒かかるアプリに initialDelaySeconds 10 の liveness を掛けておくと、永遠に立ち上がりません。この場合 initialDelaySeconds を増やすのは応急処置であり、正解は startupProbe です。詳しい計算は プローブ 3 種の設計 編に整理しました。

誤った command と entrypoint

Message に実行ファイルを見つけられなかったという文がそのまま出てきます。

kubectl describe pod migrate-runner-0 -n payments | grep -A2 "Last State"

    Last State:     Terminated
      Reason:       StartError
      Message:      failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "/app/entrypoint.sh": permission denied

権限の問題なら 126、ファイル自体がなければ 127 です。distroless イメージに command: ["sh", "-c", ...] を書くと、シェルがないので 127 が出ます。

ボリュームマウント失敗と init コンテナ

コンテナが起動すらできなかったなら、ログではなく Events を見るべきです。

kubectl describe pod ledger-0 -n payments | tail -8

Events:
  Type     Reason       Age                 From               Message
  ----     ------       ----                ----               -------
  Warning  FailedMount  2m15s (x9 over 12m) kubelet            MountVolume.SetUp failed for volume "ledger-data" : rpc error: code = Internal desc = volume attachment is being deleted
  Warning  FailedMount  38s                 kubelet            Unable to attach or mount volumes: unmounted volumes=[ledger-data], unattached volumes=[ledger-data kube-api-access-x9k2m]: timed out waiting for the condition

init コンテナが失敗すると STATUS 列に Init という接頭辞が付き、メインコンテナはまったく実行されません。ログを見るときはコンテナ名を必ず指定する必要があります。

kubectl get pod ledger-0 -n payments

NAME       READY   STATUS                  RESTARTS      AGE
ledger-0   0/1     Init:CrashLoopBackOff   4 (48s ago)   3m

kubectl logs ledger-0 -n payments -c wait-for-db --previous

waiting for postgres.payments.svc.cluster.local:5432 ...
timeout after 30s

コンテナを生かしたまま中に入る

ログだけで解けないなら、死んでいくコンテナの中を直接見る必要があります。方法は二つあり、使いどころが異なります。

第一に、command を上書きしてプロセスの代わりに眠らせておく方法です。ファイルシステム、環境変数、DNS のすべてを元とまったく同じ条件で確認できます。

apiVersion: v1
kind: Pod
metadata:
  name: checkout-shell
  namespace: payments
spec:
  restartPolicy: Never
  containers:
    - name: checkout
      image: registry.example.com/checkout:1.14.2
      command: ['sh', '-c', 'sleep infinity']
      envFrom:
        - secretRef:
            name: payment-keys
      volumeMounts:
        - name: config
          mountPath: /etc/checkout
  volumes:
    - name: config
      configMap:
        name: checkout-config
kubectl apply -f checkout-shell.yaml
kubectl exec -it checkout-shell -n payments -- sh

/ # env | grep PAYMENT
PAYMENT_API_URL=https://api.example.com
PAYMENT_WEBHOOK_SECRET=s3cr3t
/ # /app/checkout
ERROR config: required key PAYMENT_SIGNING_KEY is not set

既存の Deployment に触れずに同じ効果を出すには、複製を作ります。

kubectl debug checkout-7d9f6c5b4d-2xk9p -n payments \
  --copy-to=checkout-debug \
  --container=checkout \
  -- sleep infinity

kubectl exec -it checkout-debug -n payments -- sh

第二に、エフェメラルコンテナです。元のイメージにシェルがない distroless 環境で特に有用です。対象コンテナとプロセスネームスペースを共有するので、死につつあるプロセスも観察できます。

kubectl debug -it checkout-7d9f6c5b4d-2xk9p -n payments \
  --image=busybox:1.36 \
  --target=checkout \
  -- sh

Defaulting debug container name to debugger-7v2mp.
/ # ls /proc
1  14  self ...
/ # cat /proc/1/cmdline
/app/checkout
/ # wget -qO- http://localhost:8080/healthz
wget: can't connect to remote host: Connection refused

エフェメラルコンテナは Pod スペックを変更しないので、再起動を引き起こしません。ただしボリュームは既定では共有されないため、マウントされたファイルを見る必要があるなら --target で指定したコンテナのファイルシステムに /proc/1/root パスでアクセスします。

/ # ls /proc/1/root/etc/checkout
application.yaml  logging.yaml

再発防止 — クラッシュがデプロイパイプラインで露呈するように

同じ事故を繰り返さないためには、三か所に仕組みを付けます。

第一に、アプリケーションが起動時点で設定をすべて検証し、明確なメッセージとともに死ぬようにします。必須キーをランタイムの最初のリクエストではじめて読むコードは、CrashLoopBackOff ではなく 500 エラーとして現れ、はるかに遅く発見されます。

第二に、デプロイコマンドが失敗を待ってから返るようにします。kubectl apply はリソースを投入してすぐ成功を返すため、CI は緑なのに本番は死んでいるという状態が作られます。

kubectl apply -f deploy/checkout.yaml
kubectl rollout status deployment/checkout -n payments --timeout=180s

Waiting for deployment "checkout" rollout to finish: 0 of 3 updated replicas are available...
error: deployment "checkout" exceeded its progress deadline

Deployment に progressDeadlineSeconds を明示しておけば、この判定が自動的に付きます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: payments
spec:
  replicas: 3
  progressDeadlineSeconds: 180
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

maxUnavailable: 0 は、新しい Pod が Ready になるまで既存の Pod を殺さないようにします。クラッシュする新バージョンをデプロイしてもサービスは維持されます。

第三に、再起動の増加をアラートで捕まえます。CrashLoopBackOff は目立ちますが、先に述べた「10 分ごとに静かに再起動」はダッシュボードを見なければ何週間も放置されます。

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pod-restart-alerts
  namespace: monitoring
spec:
  groups:
    - name: pod-health
      rules:
        - alert: PodRestartingRepeatedly
          expr: increase(kube_pod_container_status_restarts_total[1h]) > 3
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "{{ $labels.namespace }}/{{ $labels.pod }} restarted more than 3 times in an hour"
        - alert: PodInCrashLoop
          expr: kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"} == 1
          for: 5m
          labels:
            severity: critical

おわりに — ログが空なら previous を先に打ちます

CrashLoopBackOff の診断は才能ではなく順序です。describe で Last State の Reason と Exit Code を確認し、kubectl logs --previous で死んだコンテナの最後の言葉を聞き、それでも出てこなければ command を上書きしてコンテナを生かしたまま中に入ります。この三段階でたいていの事故は 10 分以内に終わります。

覚えておくべき一文はこれです。CrashLoopBackOff は診断名ではなく待合室の名前であり、本当の診断名はいつも Exit Code と直前のコンテナのログに書かれています。