Claude CodeでKubernetesのrequests・limitsを実測から見直す
kubectl topとPrometheusの実測値をClaude Codeに渡し、過大なrequestsとOOMKilledを招くlimitsをmanifestで直す手順。kubectlは読み取りだけ許可します。
Podのrequestsとlimitsは、最初に置いた値のまま残りがちです。過大なrequestsはノードの空きを食いつぶし、低すぎるメモリlimitsはOOMKilledで再起動を繰り返します。この記事では、kubectl topとPrometheusの実測値をClaude Codeに読ませ、manifestの数値を直すところまでを順に進めます。kubectlは読み取りだけを許可し、適用は人が行う構成です。
kubectl topの数字だけで決めてはいけない理由
kubectl top podが返すのは、ある瞬間の使用量です。メトリクスを集めているのはmetrics-serverで、返る値のウィンドウは例のレスポンスで30sと示されています。
- CPUは、カーネルの累積カウンタから取った平均のコア使用量です
- メモリは、収集した瞬間のワーキングセット(解放できない使用中メモリの推定値)です
- 作成直後のPodは、メトリクスパイプラインの遅れで数分間は値が出ないことがあります
つまりtopの1回分は、夜間バッチやピーク時の山を拾えません。見直しの根拠に使うなら、一定期間のp95や最大値が要ります。topは「今の様子を見る」道具、Prometheusは「期間の分布を見る」道具と分けると整理できます。
| 手段 | 見えるもの | 向く判断 |
|---|---|---|
kubectl top pod --containers | 見えるもの直近ウィンドウの平均CPUと現在のメモリ | 向く判断今まさに過大・過小かの当たりをつける |
| Prometheusの期間集計 | 見えるもの数日分のp95・最大 | 向く判断requestsとlimitsの具体値を決める |
kubectl describe pod | 見えるもの直前の終了理由と再起動回数 | 向く判断OOMKilledが実際に起きたかの確認 |
requestsとlimitsは「効き方」が違う
数値を直す前に、どちらが何に効くかを押さえます。ここを取り違えると、直したつもりで症状が変わりません。
リソース設定が効く場所
CPU request
スケジューリングの判断材料になります。CPUを奪い合うときは、requestが大きいコンテナほど多くの時間をもらえる重みとして働きます。
CPU limit
超えるとカーネルがCPUへのアクセスを絞ります。スロットリングで遅くなるだけで、コンテナは終了しません。
メモリ request
主にスケジューリングで使われます。ノードのメモリが逼迫したとき、requestを超えて使っているPodが退避の候補になります。
メモリ limit
超えるとカーネルのOOM処理が働き、コンテナ内のプロセスが止められます。それがPID 1でコンテナが再起動可能なら、Kubernetesが再起動します。
スケジューラはノードの実使用量ではなく、requestsの合計で配置を判断します。使用量が低くても、requestsが大きければ別のPodは載りません。過大なrequestsの害はここにあります。
一方、メモリlimitsの強制は事後的です。超過した瞬間に必ず殺されるのではなく、カーネルがメモリ逼迫を検知したときに終了させます。「いつもは平気なのに、あるタイミングでだけ落ちる」症状は、この性質から出ます。
Claude Codeに渡す前にkubectlの権限を絞る
実測の収集は、クラスタを読むだけで済みます。そこでClaude Codeには読み取り系のkubectlだけを許可し、書き込み系は確認を残します。
組み込みの読み取り専用コマンドとして文書が例に挙げる一覧(ls、cat、grep、読み取り形のgitなど)に、kubectlは入っていません。許可しなければ毎回確認が出るので、プロジェクトの.claude/settings.jsonに書きます。
{
"permissions": {
"allow": [
"Bash(kubectl top *)",
"Bash(kubectl get *)",
"Bash(kubectl describe *)",
"Bash(kubectl diff *)"
],
"deny": [
"Bash(kubectl get secret*)",
"Bash(kubectl apply *)",
"Bash(kubectl patch *)",
"Bash(kubectl edit *)",
"Bash(kubectl scale *)",
"Bash(kubectl delete *)"
]
}
}書き方で押さえる点は3つあります。
*はサブコマンドの後ろに置きます。ワイルドカードより前の部分が、書かれたとおりに照合されるためです- 末尾の
*の前にスペースがあると、引数なしのkubectl topにも一致します。スペースを抜いたkubectl get secret*はsecretsにも一致するので、denyにはこの形が向きます &&や;でつないだ複合コマンドは、サブコマンドごとに別々に照合されます
kubectl getは読み取りですが、Secretの中身も出せます。そのためdenyでget secretを外しています。ただしdenyは「Claudeが普段書く形」を止める仕組みで、境界ではありません。kubectl -n prod get secret xのようにオプションを前に置いた書き方や、sh -c経由の呼び出しは別の形になります。本番クラスタに繋ぐなら、読み取り専用のRBACを持つ認証情報を使うのが確実です。設計はKubernetes MCPサーバーの権限設計と同じ考え方で、kubectl側のkubeconfigにも当てはまります。
もう一点あります。kubectl top pod -n prodのように、サブコマンドの直後から書かせる書式をCLAUDE.mdで決めておくと、許可ルールに一致しやすくなります。プロジェクトの.claude/settings.jsonのallowルールは、フォルダーを信頼するダイアログを受け入れた後にだけ有効になる点にも注意してください。
書き込み範囲そのものをOS側で縛る方法は、allowWrite/denyWriteでkubectl・terraformの書き込み範囲を調整するにあります。
実測値を集めてClaude Codeに渡す
まず現在の様子をtopで取ります。コンテナ単位で、メモリの大きい順に並べます。
kubectl top pod -n prod --containers --sort-by=memory
kubectl describe pod -n prod -l app=api | grep -A 6 "Last State"--containersはPod内のコンテナ別の使用量を出し、--sort-byにはcpuかmemoryを指定できます。describeのLast Stateは、直前の終了理由(OOMKilledなど)と終了コード、Restart Countを見る場所です。
次にPrometheusで、直近7日のp95を取ります。以下は例のクエリです。環境のラベル名に合わせて読み替えます。メモリの指標は、topと同じワーキングセットを指すcontainer_memory_working_set_bytesを使っています。
# メモリ: コンテナごとの7日間p95(Pod間の最大)
max by (container) (
quantile_over_time(0.95,
container_memory_working_set_bytes{namespace="prod"}[7d])
)
# メモリ: 7日間の最大値
max by (container) (
max_over_time(
container_memory_working_set_bytes{namespace="prod"}[7d])
)
# CPU: 5分レートの7日間p95
max by (container) (
quantile_over_time(0.95,
rate(container_cpu_usage_seconds_total{namespace="prod"}[5m])[7d:5m])
)結果はCSVかJSONで保存し、manifestと並べて渡します。Claude Codeにクラスタ内のPrometheusへ直接アクセスさせる必要はありません。ファイルにしておけば、同じ数字を何度でも読み直せます。
manifestはdeploy/api.yamlです。usage-7d.csvに、コンテナごとの
メモリ(最大・p95)とCPU(p95)の実測を入れました。
- requestsは実測p95を基準に、過大なものを下げる案を出す
- メモリlimitsは最大値より低いコンテナを洗い出す
- 変更はmanifestの編集だけ。kubectl applyは実行しない
根拠として、各コンテナの現在値と実測値を表にして示す最後の一文が効きます。表を先に出させると、数字の根拠を人が確認できます。manifestを書き換える前に、方針の誤りを止められます。
値の決め方をルールにしておく
数字の決め方は、チームで一度ルールにしておくと毎回ぶれません。以下は一例で、倍率は環境の負荷の揺れに合わせて変える前提の目安です。公式が示す数値ではありません。
| 項目 | 例のルール | 理由 |
|---|---|---|
| メモリrequest | 例のルール期間p95に少し余裕を足す | 理由スケジュール用の予約なので、平常時に近く置く |
| メモリlimit | 例のルール期間の最大値に余裕を足す | 理由超えるとOOMで止まるので、最大を下回らせない |
| CPU request | 例のルール期間p95を基準にする | 理由競合時の取り分の重みになる |
| CPU limit | 例のルール置かない、または大きめに置く | 理由超過はスロットリングで遅くなるだけで、終了はしない |
CPU limitを置くかどうかは、公式文書が決める話ではありません。スロットリングで遅くなるのを避けたいか、ノードの取り合いを抑えたいかという運用方針の問題です。
この表をCLAUDE.mdに書いておけば、Claude Codeは毎回同じ基準でmanifestを直します。
## リソース設定のルール
- requests/limitsは実測CSVから決める。実測が無い値は推測で書かない
- メモリlimitは期間の最大値を下回らせない
- memoryの単位はMi/Giを使う(`400m`は0.4バイトの意味になる)
- kubectlはtop/get/describe/diffのみ。applyは人が実行する直したmanifestを適用前に確かめる
例として、実測で次のことが分かったコンテナを考えます(数値は架空の例です)。
- メモリlimitは512Miで、7日間の最大は480Mi、ときどき再起動して
OOMKilledが出る - メモリrequestは2Giで、p95は300Mi前後
- CPU requestは1000mで、p95は120m
実測を反映した書き換え例
直す前
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 1000m
memory: 512Mi直した後
resources:
requests:
cpu: 150m
memory: 350Mi
limits:
memory: 768Mirequestsを実測に近づけ、メモリlimitは最大値の480Miより上に引き上げています。CPU limitを外した判断は前節のルールに従ったものです。外すかどうかは運用に合わせて決めます。
Claude Codeが書き換えたら、適用の前に差分を見ます。ここでも使うのは読み取り系だけです。
kubectl diff -f deploy/api.yamldiffと--dry-runの違い、適用前の検証順序はClaude CodeでKubernetesマニフェストを書く手順で扱っています。ここでは適用そのものを人に残し、差分を見てからkubectl applyを実行します。
落とし穴
limitsだけ書くとrequestsも同じ値になる
limitsを指定してrequestsを書かず、デフォルトの適用機構もない場合、Kubernetesはlimitsの値をrequestsとして使います。上の書き換え例はrequestsを別に書いているので、この挙動には当たりません。limitsだけを書いたmanifestは、requestsが意図せず大きくなります。
もう一つの注意点はmemory: 400mのような書き間違いです。mは千分の一を表す接尾辞なので、メモリでは0.4バイトを意味します。400MiBのつもりなら400Miです。実測のCSVからmanifestへ写すとき、Claude Codeが単位を取り違えていないかをdiffで見ます。
直したあとに確かめること
適用後は、直す前に見ていた3か所を同じ手順で見ます。
kubectl describe podのLast Stateに新しいOOMKilledが出ていないか、Restart Countが増えていないか- Pendingのまま残るPodがないか。理由が
FailedSchedulingでinsufficient cpuなどなら、requestsに対してノードの空きが足りていません - 数日後にPrometheusの期間集計をもう一度取り、新しいrequestsとのずれを見る
requestsを大きく下げたときは、ノードが逼迫した際にrequestsを超えて使うPodが退避候補になります。QoSクラスは、全コンテナでrequestsとlimitsが等しいとGuaranteed、どちらかを持つがその条件に届かないとBurstable、どちらも無いとBestEffortです。退避の順序はBestEffort、Burstable、Guaranteedの順と説明されています。
値を変えるときは、Deploymentなどのテンプレートを更新してPodを作り直す方法が、どのバージョンでも使えます。Kubernetes v1.35でStableになったin-place resizeを使えば、実行中のPodのCPU・メモリを再作成なしで変えられます。Podの/resizeサブリソースを使うので、読み取り専用の権限ではできません。この変更だけは、権限を渡す前に人が内容を見る場面です。
見直しで引っかかる点
- topの値が出ない: 作成直後は数分間メトリクスが出ません。metrics-serverが入っていないクラスタでは
kubectl top自体が使えません - memory-backed emptyDirがメモリを使う:
medium: MemoryのemptyDirはコンテナのメモリ使用に数えられます。sizeLimitを決めないと、Podのメモリlimitまで使い得ます - メモリlimitの根拠をtopだけにしない: ワーキングセットにはキャッシュ由来のページが含まれることがあり、瞬間値は小さく出ることがあります
OOMKilledが「limitsが低いだけ」とは限りません。アプリがメモリを解放していないなら、limitsを上げるのは延命にしかなりません。Kubernetes側の文書も、アプリのコードのメモリリークを先に疑い、挙動が想定どおりなら上限を上げるよう勧めています。Claude Code自身のメモリ急増とは別の話ですが、プロセスのメモリを追う発想はClaude Codeのメモリリークで使用量が急増しOOM Killされる原因と対処と通じます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。