Claude CodeでCrashLoopBackOffを読み取り専用で切り分ける
kubectlのdescribe・logs --previous・get eventsだけを許可し、apply・delete・execをdenyして、CrashLoopBackOffとImagePullBackOffを切り分けます。
PodがCrashLoopBackOffやImagePullBackOffで止まったとき、Claude Codeに原因を探させる場面は多いはずです。ただし調査のつもりでkubectlを許可すると、deleteやexecまで同じ口から通ります。ここでは、読み取りの3コマンドだけを通し、書き込み系をdenyで塞いだうえで、2つの状態を切り分ける手順を書きます。
先に結論です。ImagePullBackOffはイメージが取れない問題、CrashLoopBackOffはコンテナが起動しては落ちる問題で、見る場所が違います。前者はdescribeのEvents、後者はlogs --previousです。
ImagePullBackOffとCrashLoopBackOffは何が違うのか
どちらも名前にBackOffが付くため、同じ種類の障害に見えます。実際は、コンテナが始まる前に失敗しているか、始まった後に失敗しているかで分かれます。
| 状態 | 何が起きているか | 最初に見る出力 |
|---|---|---|
| ImagePullBackOff | 何が起きているかイメージを取得できず、コンテナが始まらない | 最初に見る出力kubectl describe podのEvents |
| CrashLoopBackOff | 何が起きているかコンテナが起動しては失敗し、待ち時間を延ばしながら再起動されている | 最初に見る出力kubectl logs --previous |
ImagePullBackOffの原因として、Kubernetesのドキュメントは「イメージ名が正しくない」「imagePullSecretなしでプライベートレジストリから取得している」を挙げています。再試行の待ち時間は段階的に延び、上限は300秒(5分)です。
CrashLoopBackOffの原因は、公式のPodライフサイクルの説明に5つ並んでいます。アプリケーションのエラー、環境変数や設定ファイルの誤り、CPUやメモリの不足、起動が間に合わないことによるヘルスチェックの失敗、livenessやstartupプローブの失敗です。
もう1つ押さえておく点があります。CrashLoopBackOffは、kubectlの表示欄(STATUS)に出る文字列です。ドキュメントは、Podのphaseとは別物として扱っています。Claude Codeに「phaseがCrashLoopBackOffのPod」と伝えると話がずれるので、STATUS列の表示として渡します。
読み取り専用で渡す3つのコマンド
調査に使うのは、次の3つで足ります。
kubectl describe pod my-pod -n my-namespace
kubectl logs my-pod -n my-namespace --previous
kubectl get events -n my-namespace --sort-by=.metadata.creationTimestampdescribe podはコンテナの状態と直近のイベントを返します。Kubernetesのデバッグ手順も、最初の一手としてこのコマンドを挙げています。logs --previousは、再起動前のコンテナのログを読みます。クラッシュした本人のログは、現在のコンテナには残っていないからです。get eventsは、--sort-byで時系列に並べられます。
複数コンテナのPodなら、logsに-c <コンテナ名>を付けます。Deploymentから引くならkubectl logs deploy/my-deployment --previousの形も使えます。
この3つは、どれも何も変更しません。変更しないからこそ、Claude Codeへの許可は「この形のコマンドだけ」と絞れます。
kubectlの書き込み系を塞ぐ権限の切り方
組み込みの読み取り専用コマンドに、kubectlは入らない
Claude Codeには、確認なしで走る読み取り専用コマンドの組み込みセットがあります。ls、cat、grep、find、diff、gitの読み取り形などで、設定では変えられません。この一覧にkubectlは載っていません。何もしなければkubectl getでも確認が出ます。
確認を減らすにはallowに書き、書き込み系はdenyに書きます。.claude/settings.jsonの例です。
{
"permissions": {
"allow": [
"Bash(kubectl get pods *)",
"Bash(kubectl get events *)",
"Bash(kubectl describe pod *)",
"Bash(kubectl logs *)"
],
"deny": [
"Bash(kubectl apply *)",
"Bash(kubectl * apply *)",
"Bash(kubectl delete *)",
"Bash(kubectl * delete *)",
"Bash(kubectl exec *)",
"Bash(kubectl * exec *)",
"Bash(kubectl get secret*)",
"Bash(kubectl * get secret*)"
]
}
}denyが2行ずつ並んでいるのには理由があります。
denyを2行ずつ書く理由
Bashのルールは、*の手前の文字列をそのまま照合します。*の前の空白もルールの一部です。kubectl delete *はkubectl delete pod xに当たりますが、kubectl -n prod delete pod xには当たりません。グローバルフラグがdeleteの前に入ると、先頭が合わなくなるからです。
公式ドキュメントにも、Bash(git push *)がgit -C . push origin mainを止めない例が表で載っています。そこでkubectl * delete *の形を足します。*が-n prodのようなフラグを受け持ちます。ただしkubectl delete pod xのように間が空かない形には、空白の分だけ当たらない可能性があるため、素の形も別に書きます。
allow側は逆に、狭いほど安全です。Bash(kubectl describe pod *)はkubectl -n prod describe pod xに当たりません。その場合は確認が出るだけで、書き込みが通ることはありません。許可が当たらなかったときの失敗が「確認が出る」で済むので、allowはkubectlの直後にサブコマンドを置く形に固定します。名前空間は-n my-namespaceをコマンドの後ろに付ける運用にそろえると、確認を増やさずに済みます。
Bash(kubectl get secret*)は、末尾の*の前に空白がありません。kubectl get secretとkubectl get secretsの両方に当たります。公式のls*とlsofの例と同じ考え方です。Secretの中身をClaude Codeに読ませないための行になります。
ルールの優先順位
denyは、どの設定スコープに書いてもallowに勝ちます。ユーザー設定で許可していても、プロジェクト設定のdenyが優先されます。チームで配るなら、プロジェクトの.claude/settings.jsonにdenyを置けば、個人の設定では外せません。管理設定(managed settings)のdenyは、さらに上位です。
パイプとリダイレクト
調査ではkubectl logs my-pod --previous | grep -i errorのようにパイプを使いたくなります。複合コマンドは、サブコマンドごとに別々に照合されます。kubectl logs *の許可と、読み取り専用の組み込みセットに入るgrepで、全体が確認なしで通ります。
出力をファイルに落とす> logs.txtは別の扱いです。リダイレクト先が、Editのルールと作業ディレクトリの範囲で検査されます。
落とし穴
denyはkubectlの別の呼び方を止めない
公式ドキュメントは、Bashのdenyが絶対の境界ではないと明記しています。rm *をdenyしても/bin/rm -rf build/やbash -c 'rm -rf build/'は止まりません。kubectlにも同じことが当てはまり、/usr/local/bin/kubectl delete ...やsh -c 'kubectl delete ...'は文字列として別物です。
本当に書き込ませない境界は、権限ルールの外に置きます。読み取り専用のRBACを持つkubeconfigだけを渡す方法は、Kubernetes MCPサーバーの権限設計で扱っています。コマンド全文を検査するPreToolUseフックで止める方法もあります。
切り分けの流れ
Claude Codeに渡す順番を決めておくと、出力を貼り替える回数が減ります。上から順に確かめます。
Podが起動しないときの確かめ順
- Qkubectl get podsのSTATUSがImagePullBackOffか?答えがいいえのとき答えがはい
はい:
kubectl describe podのEventsで、イメージ名の綴りとタグ、レジストリへのpush済みかどうか、プライベートレジストリならimagePullSecretの有無を見ます。Kubernetesのデバッグ手順にある、手元でdocker pull <image>を試す確認も有効です。 - QSTATUSがCrashLoopBackOffで、RESTARTSが増え続けているか?答えがいいえのとき答えがはい
はい:
kubectl logs <pod> --previousでクラッシュ直前のログを読みます。ログが空なら、起動直後に落ちているか、プローブの失敗で再起動されている可能性があります。describe podのプローブ設定とEventsを重ねます。 - QPendingのままで、コンテナがまだ始まっていないか?答えがいいえのとき答えがはい
はい: スケジューリングの問題です。
describe podにスケジューラーのメッセージが出ます。CPUやメモリの不足、hostPortの使用が公式の挙げる原因です。
原因の仮説が1つに絞れた
絞れたら、そこで読み取りの調査を終えます。直す作業は、権限を分けた別のセッションで行います。
CrashLoopBackOffでは、待ち時間の仕組みも頭に置いておきます。落ちるたびに再起動の間隔は延び、成功して一定時間(公式の例では10分)動けば待ち時間が最初に戻ります。kubectl get podsを何度か叩くと、RESTARTSが増える速さが落ちていくのは、この指数的な待ちのためです。
Claude Codeに渡す指示の書き方
権限だけでなく、調査の手順もCLAUDE.mdに書いておくと、毎回の指示が短くなります。例えば次のような形です。
## Kubernetes調査のルール
- 使ってよいのは get pods / get events / describe pod / logs だけ
- CrashLoopBackOff は logs --previous を先に読む
- ImagePullBackOff は describe pod の Events を先に読む
- 出力に認証情報らしき文字列があれば、引用せず該当箇所だけ報告する
- 修正案はYAMLの差分で示し、apply はしないこの文面は、CLAUDE.mdに書く例です。ここに書いた内容はClaude Codeの判断を方向づけますが、強制はしません。強制する部分は、前節のdenyが受け持ちます。
実際の依頼は、出力を貼る形にすると確実です。
my-pod が CrashLoopBackOff です。kubectl logs my-pod --previous と
kubectl describe pod my-pod の結果を読み、原因の候補を3つまで挙げて。
それぞれ、どの出力のどの行が根拠かも示して。「候補を3つまで」「根拠の行」を指定するのは、推測を断定の形で返させないためです。出力にない情報から原因を言い切ることを避けられます。
直す段になったら権限を分ける
原因が絞れたら、修正はマニフェストの変更です。Deploymentのイメージタグを直す、resourcesやlivenessProbeを調整するといった作業になります。書く手順はClaude CodeでKubernetesマニフェストを書く手順に譲り、kubectl diffと--dry-runで確かめてから、applyは人が実行する形が収まりのよい分担です。
kubectlやterraformがkubeconfigやstateへ書けずに止まる場合は、サンドボックスの書き込み範囲の問題です。これはallowWrite/denyWriteの調整で扱っています。
Runnerのように短時間で終わる正常終了を繰り返すコンテナも、CrashLoopBackOffに見えることがあります。その事例はセルフホストEnvironmentのデプロイ手順にあります。
まとめ
ImagePullBackOffはdescribeのEvents、CrashLoopBackOffはlogs --previousと、最初に開く出力が決まっています。Claude Codeに許すkubectlはその3コマンドに絞り、apply・delete・execはkubectlの直後と、フラグを挟んだ形の両方をdenyに書きます。denyは別経路を止めないため、クラスタ側の読み取り専用kubeconfigと組み合わせて初めて、書き込ませない構成になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。