Claude CodeとOpenCostでKubernetesのrequests削減案を作る
OpenCostのallocation APIで要求過多のワークロードを洗い出し、削減後のrequestsをkustomizeとHelm valuesの差分にします。HPAとPDBへの影響も、適用前に確かめます。
Kubernetesのrequestsは、使われなくても予約され続けます。OpenCostはその予約分をコストとして見せてくれるので、「どのワークロードが実使用より大きく確保しているか」を金額順に並べられます。この記事では、OpenCostのallocation APIの値をClaude Codeに読ませ、削減後のrequestsをkustomizeのパッチとHelm valuesの差分にするところまでを進めます。最後に、requestsを下げると動きが変わるHPAと、ドレイン時に効くPDBを適用前に確かめます。
kubectl topの実測から値を見直す一般的な手順は、Claude Codeでrequests・limitsを実測から見直すにあります。ここでは、コストの大きい順に着手先を決める部分に絞ります。
OpenCostのコストは「requestと使用量の大きいほう」で決まる
OpenCostの仕様では、ワークロードのコストはmax(request, usage)として扱われます。CPUもメモリも、要求量と使用量のうち大きいほうが課金対象です。
つまり、requestsを大きく取ったコンテナは、使っていなくてもその分のコストが乗ります。逆にrequestsより多く使っているコンテナは、使用量のほうで計上されます。削減の余地が大きいのは、前者の「要求が使用量を大きく上回る」コンテナです。
もう一つ押さえる点があります。allocation APIの金額はオンデマンドの定価ベースの計算で、請求書の額そのものではありません。削減額を約束する数字ではなく、着手の優先順位をつけるための物差しとして使います。
OpenCostの値をClaude Codeに渡す2つの経路
渡し方は2つあります。どちらも、クラスタへの書き込みは伴いません。
OpenCostの値の渡し方
curlでJSONを保存する
port-forwardしたAPIを人が叩き、結果をファイルに保存して渡します。同じ数字を何度でも読み直せ、権限設計も単純です。
OpenCostのMCPサーバーを繋ぐ
Claude Codeがget_allocation_costsを直接呼びます。集計軸を変えて聞き直すときに速い一方、出力サイズの上限に当たりやすくなります。
curlで保存する
OpenCostのAPIは、既定でポート9003にあります。別ターミナルでport-forwardを張り、7日分をコンテナ単位で保存します。
kubectl -n opencost port-forward deployment/opencost 9003
curl -G http://localhost:9003/allocation \
-d window=7d -d aggregate=container -d resolution=1m \
> alloc-7d.jsonwindowには7dのほかlastweekやmonthも指定できます。resolutionの既定は1mで、細かいほど正確ですが重くなります。短命なジョブを含むなら粗い値は避けます。
応答のフィールド名は、バージョンや設定で増減します。Claude Codeにいきなり集計させず、まず中身を確かめさせると外れません。
jq '.data[0] | to_entries[0].value | keys' alloc-7d.json出てきたキーの中から、CPU・メモリそれぞれの「要求量」「使用量」「効率」にあたるものをClaude Codeに選ばせます。ここで選んだキー名を、次のプロンプトとCLAUDE.mdに固定します。
OpenCostのMCPサーバーに繋ぐ
OpenCostにはMCPサーバーが同梱されており、Helmチャートの既定で有効、ポートは8081です。使えるツールはget_allocation_costs、get_asset_costs、get_cloud_costsの3つです。requestsの見直しで使うのは、windowとaggregateを取るget_allocation_costsです。
kubectl -n opencost port-forward svc/opencost 8081:8081
claude mcp add --transport http --scope project \
opencost http://localhost:8081--scope projectにすると.mcp.jsonに書かれ、チームで同じ接続を共有できます。ただしプロジェクトスコープのサーバーは、初回に承認を求められます。
権限は、読み取り専用のOpenCostのツールだけを許可します。MCPツールの許可ルールはmcp__<サーバー名>__<ツール名>の形で書きます。
{
"permissions": {
"allow": ["mcp__opencost__get_allocation_costs"],
"deny": [
"Bash(kubectl apply *)",
"Bash(kubectl patch *)",
"Bash(kubectl edit *)",
"Bash(kubectl scale *)",
"Bash(kubectl delete *)"
]
}
}コンテナ単位で全クラスタを引くと、結果が大きくなります。Claude Codeは、MCPの出力が10,000トークンを超えると警告を出し、既定では25,000トークンで切ります。50,000文字を超えるテキストはファイルに保存されます。最初はaggregate=namespace,controllerのように粗く引き、候補の名前空間だけコンテナ単位に掘る順が安全です。
要求過多のワークロードを洗い出す
保存したJSON(またはMCPの結果)を渡して、候補を出させます。判断基準を先に書いておくと、出力が揃います。
alloc-7d.json は OpenCost の allocation(7日、コンテナ単位)です。
キーは上で確認した次の名前を使います: <要求量・使用量のキー名>
- CPU・メモリそれぞれで、要求量に対する使用量の比が低い順に並べる
- 使用量が極端に小さいもの(停止中や未使用の疑い)は別表にする
- 各行に、名前空間・コントローラ・現在のrequest・7日の使用量を入れる
- 削減案は、使用量の平均ではなく kubectl top とPrometheusの
p95・最大値で裏を取ってから出す。まだ数値は書き換えない最後の一行が肝です。allocation APIが返すのは、windowで区切った期間の集計値です。集計に合わせてrequestsを切ると、夜間バッチやピークで足りなくなるおそれがあります。候補を絞るのにはOpenCost、値を決めるのには期間のp95と最大値、と役割を分けます。
候補が出たら、上位のワークロードだけkubectl top pod --containersで現在値を取り、Prometheusの期間集計と突き合わせます。この裏取りの手順とクエリは、先の実測から見直す記事に書いています。
削減後のrequestsを差分にする
数字が決まったら、manifestを直接書き換えさせず、管理している単位の差分にします。数値は架空の例です。実測でp95がCPU 120m、メモリ300Mi前後、最大でメモリ420Miだったコンテナを考えます。
requestsの書き換え例(架空の数値)
現在
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
memory: 2Gi削減案
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Giメモリのlimitsは、実測の最大(420Mi)を下回らない値にしています。requestsだけを下げて、limitsを触らない選択肢もあります。コスト起点の見直しで効くのはrequestsなので、limitsは別の判断で動かします。
kustomizeならパッチ1枚にする
overlayを使っているなら、変更はパッチ1枚に収めます。overlayの組み方はkustomizeのoverlayを組む手順にあります。
# overlays/prod/resources-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
resources:
requests:
cpu: 250m
memory: 512Mikustomization.yamlのpatchesに、このファイルを足します。
Helmならvaluesの差分にする
Helmチャートなら、チャートは触らずvaluesだけを変えます。
# values-prod.yaml(差分)
resources:
requests:
cpu: 250m
memory: 512Mi適用前に本番との差分を見る
どちらも、kubectl diffで「適用したらどうなるか」を読み取りだけで確かめます。差分があると終了コードが1になるので、スクリプトに組むときは0と1を区別します。
kubectl diff -k overlays/prod
helm template api ./chart -f values-prod.yaml | kubectl diff -f -Helmのアップグレード単位で差分をレビューしたいときは、helm diffでアップグレード前の差分を見る手順が使えます。適用そのものは人が行い、Claude Codeには差分の確認までを任せます。
HPAとPDBへの影響を確かめる
requestsを下げるときに、見落としやすいのが次の2点です。
HPAは「requestsに対する比率」で動く
HPAのリソース使用率は、現在の使用量とrequestsの比で計算されます。requestsを下げると、同じ使用量でも使用率の数字が上がります。
先の例で、CPUの使用量が120mのまま、requestを1000mから250mに下げたとします。使用率は12%から48%に変わります。HPAの目標使用率が60%なら、余裕は大きく減ります。ピークで200mを使えば80%になり、レプリカが増えます。
落とし穴
requestsを下げるとレプリカ数が増えることがある
HPAの目標が使用率(averageUtilization)のワークロードは、requestsの変更でスケールの閾値も動きます。削減によるコスト減が、レプリカ増で打ち消されないか確認します。目標値を見直すか、削減幅を小さくします。
Claude Codeには、HPAのmanifestを読ませて、変更後の使用率を計算させます。
削減案の各ワークロードについて、同じnamespaceのHPAを読み、
metricsがUtilizationのものを抜き出してください。
変更後のrequestsで、7日のp95使用量時点の使用率と、
最大使用量時点の使用率を表にします。目標値を超えるものを赤く示します。PDBはレプリカ数が変わると効き方が変わる
PDBは、自発的な中断(ドレインによる退避など)で、同時に落としてよいPodの数を制限します。minAvailableやmaxUnavailableは絶対数でも割合でも指定できます。
HPAでレプリカ数が増減するワークロードは、同じPDBでも許可される退避数が変わります。変更後の姿を想定して、現在のPDBが退避を許す状態か、kubectl get pdbのALLOWED DISRUPTIONSで確かめます。ここが0のままだと、ドレインが止まります。PDBの足し方と動作確認はPodDisruptionBudgetを足してドレインを確かめる記事にまとめています。
なお、requestsを書き換えるとPodテンプレートが変わるため、Deploymentでは新しいReplicaSetへの入れ替えが始まります。適用のタイミングは、トラフィックの少ない時間帯に合わせます。
手順をCLAUDE.mdに固定する
毎回同じ順で動かすため、判断基準をCLAUDE.mdに書いておきます。
## requests削減のルール
- 候補の抽出は OpenCost の allocation(7日、コンテナ単位)を使う
- allocationは期間の集計値。値の決定はp95と最大値で行う
- メモリlimitは期間の最大値を下回らせない
- 変更はkustomizeのパッチ、またはHelm valuesの差分で出す
- 差分は kubectl diff で確認する。kubectl apply は実行しない
- HPAのUtilization目標があるワークロードは、変更後の使用率を表にするこれで、Claude Codeは同じ入口から同じ基準で案を出します。MCPで繋ぐ場合は、サーバーの接続状態をclaude mcp get opencostで確かめてから始めます。
つまずきやすい点
- 短い期間で判断する: 1日分では週次のバッチやリリース直後の山を拾えません。7日以上を基本にします
- 使用量がほぼ0のものを削る: 停止中のCronJobや、障害時だけ動くワークロードが紛れます。別表に分けて、持ち主に確認します
- 定価ベースの金額を請求額と読む: 定価ベースの計算なので、請求書の額とは一致しません。順位付けにだけ使います
- kubectl topとOpenCostの値が合わない: 前者は直近の瞬間値、後者は期間の集計です。差が出るのが普通で、見る目的が違います
まとめ
OpenCostは「どこを削るか」を決める道具で、「いくつにするか」はp95と最大値が決めます。この分担を守ると、平均値に合わせすぎて足りなくなる失敗を避けられます。削減案はkustomizeかHelm valuesの差分として出し、HPAの使用率とPDBの退避数を適用前に確かめる流れまで含めて、1つの手順にしておくと再利用できます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。