Claude CodeとplutoでKubernetesの非推奨APIを移行する手順
plutoで見つけた非推奨apiVersionをclaude -pに渡し、manifestの書き換えとkubectl convertでの差分確認、Helmチャートの仕分けまでを進める手順です。
Kubernetesのアップグレード前に、manifestに残った古いapiVersionを探して直す作業は、Claude Codeに向いています。ただし「探す」役をClaudeに任せてはいけません。探すのはplutoで、Claude Codeは見つかった行の書き換えと仕分けを受け持ちます。この記事ではpluto detect-filesの出力をclaude -pに渡し、manifestを移行して、kubectl convertで結果を突き合わせるまでの流れを作ります。
探す役と直す役を分ける理由
plutoは、コードリポジトリとHelmリリースの中から非推奨のapiVersionを探すツールです。READMEは、api-serverに「deployments.v1.appsを返して」と頼むやり方に注意を促しています。extensions/v1beta1で作ったDeploymentでも、api-serverは親切にapps/v1へ変換して返すためです。クラスタに問い合わせても、manifestに書かれた古い記述は見つかりません。
だからリポジトリ側の静的な検出はplutoに任せ、LLMには確定した検出結果だけを渡します。Claudeにリポジトリ全体を読ませて「古いAPIを探して」と頼む方法は、見落としが出ても気づけません。plutoの出力なら、件数と対象ファイルが機械的に決まります。
plutoの検出は終了コードで結果を返します。CI向けの値として、advanced.mdに次の4つが載っています。
| 終了コード | 意味 |
|---|---|
| 1 | 意味エラー(メッセージが出る) |
| 2 | 意味非推奨(deprecated)のapiVersionを検出 |
| 3 | 意味削除済み(removed)のapiVersionを検出 |
| 4 | 意味対象バージョンに置き換え先のapiVersionがない |
検出があるとコマンドが非ゼロで終わるので、パイプやスクリプトに組み込むときはset -eと相性が悪い点に注意が要ります。移行作業では、これを逆手に取って「終了コード0になるまで直す」を完了条件にできます。
plutoを入れて対象バージョンを決める
インストールはHomebrewかasdf、あるいはリリースページのバイナリです。
brew install FairwindsOps/tap/pluto
pluto versionコンテナで使う場合、v5.24.0からイメージの置き場所がus-docker.pkg.dev/fairwinds-ops/oss/plutoに移りました。旧quay.io/fairwinds/plutoは非推奨と案内されているので、CIのイメージ参照が旧い場合は先に直します。
次に、アップグレード先を--target-versions(-t)で明示します。形式はコンポーネント=バージョンで、バージョンはvで始めます。advanced.mdには既定の対象がv1.22.0と書かれています。既定に頼ると、アップグレード先が1.25や1.32のときに取りこぼすため、毎回指定する運用が安全です。
pluto detect-files -d ./k8s -t k8s=v1.32.0 -o wide「削除済みだけ見たい」ときは--only-show-removed(-r)を付けます。非推奨のまま動いている分を後回しにして、アップグレードで止まるものだけを先に潰す絞り込みです。
検出結果をclaude -pに渡して直させる
ここからがClaude Codeの出番です。claude -pは標準入力を読めるので、plutoの出力をパイプで渡せます。公式のheadlessのページにも、cat build-error.txt | claude -p '...'の形が載っています。
ファイルパスの列を含めた出力にするため、-o markdownに--columnsを足します。advanced.mdにはFILEPATH列を指定した例があり、列名に空白を含むものはクォートが必要と注記されています。
pluto detect-files -d ./k8s -t k8s=v1.32.0 \
-o markdown \
--columns FILEPATH,NAME,KIND,VERSION,REPLACEMENT,"REMOVED IN" \
> pluto-report.md
cat pluto-report.md | claude -p \
"この表の各行について、FILEPATHのmanifestのapiVersionをREPLACEMENTに書き換えて。
apiVersionの変更で必須になるフィールドや名前が変わるフィールドがあれば、
同じファイルで合わせて直す。kubectl applyは実行しない。" \
--permission-mode acceptEdits \
--allowedTools "Read,Edit"権限まわりの意味は次のとおりです。
acceptEditsは、ファイルへの書き込みを確認なしで通します。--allowedTools "Read,Edit"は、読み書きだけを明示的に許可します。- 許可していないBashは、
-pの実行では承認する相手がいないため拒否されます。
つまりこの設定では、Claudeはmanifestを書き換えられても、kubectl applyは実行できません。クラスタに触れない状態を、権限で担保した形です。標準入力の上限は10MBなので、大きなリポジトリで出力が巨大になる場合は、ディレクトリ単位に分けて回します。
apiVersionを書き換えるだけでは足りないリソース
置換だけで済むリソースは意外に少数です。Kubernetesの移行ガイドには、apiVersionを上げると同時にフィールドの扱いが変わるものがはっきり書かれています。プロンプトに「必須になるフィールドも直す」と入れたのは、このためです。
| リソース | 古い版 | 移行先 | 同時に直す点(移行ガイドの記載) |
|---|---|---|---|
| Deployment | 古い版extensions/v1beta1など | 移行先apps/v1 | 同時に直す点(移行ガイドの記載)spec.selectorが必須かつ作成後は変更不可 |
| Ingress | 古い版extensions/v1beta1、networking.k8s.io/v1beta1 | 移行先networking.k8s.io/v1 | 同時に直す点(移行ガイドの記載)serviceNameはservice.nameに、pathTypeが必須 |
| CronJob | 古い版batch/v1beta1 | 移行先batch/v1 | 同時に直す点(移行ガイドの記載)特記なし |
| HorizontalPodAutoscaler | 古い版autoscaling/v2beta1 | 移行先autoscaling/v2 | 同時に直す点(移行ガイドの記載)targetAverageUtilizationはtarget.averageUtilizationとtarget.type: Utilizationに |
| PodDisruptionBudget | 古い版policy/v1beta1 | 移行先policy/v1 | 同時に直す点(移行ガイドの記載)空のspec.selectorが全Podを選ぶ挙動に変わる |
| CustomResourceDefinition | 古い版apiextensions.k8s.io/v1beta1 | 移行先apiextensions.k8s.io/v1 | 同時に直す点(移行ガイドの記載)spec.scopeの明示、spec.versions[*].schemaへの移動 |
特に気をつけたいのがPodDisruptionBudgetです。policy/v1beta1では空のselectorが「何も選ばない」でしたが、policy/v1では「namespace内の全Podを選ぶ」に変わります。apiVersionの文字列を置換するだけだと、意図せず全Podを守る設定に化けます。Claudeの書き換え結果で、このリソースだけは差分を目で読んでください。
また、PodSecurityPolicyはpolicy/v1beta1のまま消えるリソースで、移行先のapiVersionがありません。plutoの終了コード4に当たる典型で、Pod Security Admissionなど別の仕組みへの設計変更が必要です。ここはClaudeに置換させず、方針を決めてから任せる領域です。
kubectl convertで書き換え結果を突き合わせる
Claudeの書き換えが正しいかを、Claudeの自己申告で確かめないのがこの手順の肝です。Kubernetes公式の移行ガイドは、kubectl convert -f <file> --output-version <group>/<version>で既存のobjectを変換できると案内しています。同じ変換を別の経路から出し、Claudeの結果と比べます。
kubectl convertはkubectlに最初から入っているわけではなく、プラグインとして別途インストールします。Linuxならkubectl-convertのバイナリを取得し、/usr/local/binなどに置きます。
kubectl convert --help | head -3
# 旧版manifest(gitのHEAD側)を変換し、Claudeが書いた版と比べる
git show HEAD:k8s/deploy.yaml > /tmp/old-deploy.yaml
kubectl convert -f /tmp/old-deploy.yaml --output-version apps/v1 > /tmp/converted.yaml
diff /tmp/converted.yaml k8s/deploy.yaml差分の読み方は2通りあります。
- 差分がほぼ空ならapiVersionの書き換えは機械変換と一致しています。
- 差分があるなら、Claudeが余計な値を足したか、
kubectl convertが既定値を補った可能性があります。
後者について、公式の移行ガイドは「この変換は理想的でない既定値を使うことがある」と注意しています。変換結果を正解として採用するのでなく、あくまで突き合わせの基準線として扱います。diffの出力をそのままclaude -pに渡して「意図しない差分か、既定値の補完かを分類して」と頼むのも使える形です。
Helmチャートは直す場所を仕分ける
Helmを使っている場合、plutoの入力はディレクトリではなくレンダリング結果になります。READMEの例では、helm template <chart-dir> | pluto detect -の形です。
helm template ./charts/myapp | pluto detect - -t k8s=v1.32.0 -o wideこのコマンドの出力にはファイルパスがありません。手がかりはKINDとリソース名だけなので、「どのテンプレートを直すか」はClaudeに探させる作業になります。ここで効くのが、仕分けの指示です。
直す場所は、検出の出どころで3つに分かれます。
| 出どころ | 直す場所 | Claudeに任せる範囲 |
|---|---|---|
自作チャートのtemplates/ | 直す場所テンプレート内のapiVersion行 | Claudeに任せる範囲書き換えまで任せる |
values.yamlの値でapiVersionを切り替えている | 直す場所テンプレートの条件分岐(.Capabilities.APIVersionsなど) | Claudeに任せる範囲候補の提示まで。分岐の設計は人が決める |
サブチャート・外部チャート(charts/配下、依存) | 直す場所チャートのバージョンを上げる | Claudeに任せる範囲修正対象外。依存の更新案を出させる |
この表は、Claudeに検出結果を渡す前に自分のリポジトリで埋めておくと効果的です。外部チャートの中身をClaudeに直させると、次のhelm dependency updateで変更が消えます。
仕分けそのものをClaudeに任せる場合は、--json-schemaで出力の形を固定できます。headlessのページには、スキーマを渡すとstructured_outputフィールドに結果が入る使い方が載っています。
helm template ./charts/myapp | pluto detect - -t k8s=v1.32.0 -o json \
| claude -p "検出された各リソースについて、charts/myapp配下のどのファイルが
出どころかを調べ、own(自作テンプレート)、subchart、valuesのどれかに分類して" \
--allowedTools "Read,Grep" \
--output-format json \
--json-schema '{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"file":{"type":"string"},"origin":{"type":"string","enum":["own","subchart","values"]}},"required":["kind","origin"]}}},"required":["items"]}' \
| jq '.structured_output'structured_outputは、検証済みの出力が得られなかった実行では欠けます。Claudeが依頼を断った場合などです。jqに渡す前に、フィールドの有無を確認するのが安全です。
クラスタ側の確認と、plutoの結果が消えないとき
リポジトリを直しても、すでにデプロイ済みのHelmリリースが古いapiVersionで残っていることがあります。plutoにはpluto detect-helmがあり、クラスタ内のHelmリリースを調べます。-nで名前空間を絞れます。
ここで一つ癖があります。plutoのFAQによると、manifestを直した後もplutoが古いapiVersionを報告し続けることがあります。plutoはDeployed状態のリリースのapiVersionを見ますが、Helmの不具合で古いリビジョンがDeployedのまま残る場合があるためです。対処は、helm history <release>でリビジョンの履歴を確認し、古いリビジョンのリリースSecret(sh.helm.release.v1.<release>.v<番号>)を削除してから、plutoを再実行することです。削除は元に戻せないので、Claudeには実行させず、kubectl deleteの候補を提示させるに留めます。
また、detect-api-resourcesとdetect-all-in-clusterが頼るkubectl.kubernetes.io/last-applied-configuration注釈は、kubectl patchで消える場合があります。FAQはこの方法を、非推奨APIの検出手段として信頼できないと説明しています。CI用の判定に使うなら、リポジトリ側のdetect-filesを主にして、クラスタ側は補助にとどめます。
CLAUDE.mdに移行のルールを置く
毎回プロンプトに書くより、プロジェクトのCLAUDE.mdに移行の約束事を書いておくほうが、claude -pの実行が安定します。headlessのページは、CIでは--bareを付けるとCLAUDE.mdを読み込まずに起動できると説明しているため、読み込ませたい運用なら--bareは付けません。
## Kubernetes manifestの移行ルール
- 検出にはpluto(`pluto detect-files -d ./k8s -t k8s=v1.32.0`)を使い、結果が0件になるまで直す
- apiVersion変更で必須になるフィールドは同じ変更で直す(Deploymentのspec.selector、Ingressのpath.pathTypeなど)
- PodDisruptionBudgetは空のselectorの意味が変わるので、差分をPRの説明に書く
- `charts/`配下の外部チャートは編集しない。バージョン更新の提案だけ出す
- `kubectl apply`と`helm upgrade`は実行しない最後の1行は、権限設定とは別に手順を明文化する役目です。実行の可否そのものは--allowedToolsと権限モードで決まるので、CLAUDE.mdは「やってほしいこと」の表明と考えます。
終了コードを完了条件にする
仕上げに、plutoの終了コードをゲートに使います。--only-show-removedを付けた状態で終了コード0になれば、「削除済みのapiVersionはない」状態です。
pluto detect-files -d ./k8s -t k8s=v1.32.0 -r
echo "exit=$?"これを移行ブランチのCIに入れると、Claudeが直し切れなかった分が次のPRで検出されます。非推奨のまま残すものは--ignore-deprecationsで終了コード2を無視しつつ、削除済み(終了コード3)だけで落とす構成もできます。
manifestの書き方そのものはClaude CodeでKubernetesマニフェストを書く手順に、チャートの組み立てはClaude CodeでHelmチャートを作成する手順にまとめています。claude -pの権限や--bareの扱いはclaude -pオプションの解説が詳しいです。
まとめ
非推奨APIの移行で効くのは、検出をplutoに固定し、Claudeには検出済みの行の修正だけを渡す分担です。Claudeが書いた結果はkubectl convertの出力とdiffで突き合わせ、PodDisruptionBudgetのように意味が変わるリソースだけは差分を目で読みます。Helmチャートは、自作テンプレート・values・外部チャートに仕分けてから直す場所を決めます。終了コード0を完了条件にしておけば、アップグレード当日に初めて失敗に気づく事態を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。