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

- Name
- Youngju Kim
- @fjvbn20031
はじめに — 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 pod | ConfigMap・Secret のキー名とネームスペースを確認 |
| 正常終了後に再起動を繰り返す | Exit Code 0、Reason Completed | kubectl logs POD --previous | プロセスをフォアグラウンドで実行 |
| OOMKilled | Exit Code 137、Reason OOMKilled | describe の Last State | 上限の引き上げまたはランタイムのヒープ設定調整 |
| liveness プローブによる強制終了 | Exit Code 143 または 137、Events に Unhealthy と Killing | kubectl get events --field-selector reason=Unhealthy | startupProbe の追加、しきい値の緩和 |
| 誤った command・entrypoint | Exit Code 126 または 127、Message に executable file not found | kubectl describe pod の Message | command と args の修正、イメージにシェルがあるか確認 |
| ボリュームマウント失敗 | コンテナが起動すらできない、Events に FailedMount | kubectl describe pod の Events | PVC のバインドと Secret の存在を確認 |
| init コンテナの失敗 | STATUS が Init:CrashLoopBackOff | kubectl logs POD -c INIT_NAME --previous | init ロジックの修正、依存サービスの起動順序を確認 |
設定と 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 と直前のコンテナのログに書かれています。