Kubernetesのmanifest管理をKustomizeに寄せていても、世の中の多くの製品はHelm chartで配布されています。
cert-manager、Argo CD、Harbor、Longhorn……。「HelmとKustomizeのどちらを使うべきか」という比較記事は数多くありますが、実際の答えは、たいてい両方を使うことになるです。
私のrepoでは、KustomizeがHelmを包む形、つまりhelmChartsフィールドと--enable-helmを使う構成で、20近い製品を約2年間運用してきました。
この記事では、その実運用で固まった判断基準――どの差分をvaluesに書き、どこからKustomizeのpatchで上書きし、いつchartを使い続けること自体を見直すか――を整理します。
あわせて、同じchartがrepoに6バージョン残るまで放置した失敗も書きます。
先に結論
私が現在採用している型は、次のようなものです。
- chartをrepoに同梱する
- Kustomizeの
helmChartsでmanifestを生成する - chartだけでは表現できない差分をpatchで補う
- chart、Kustomize、Helmのバージョンを意識して更新する
差分の書き方は、次の3段階で判断します。
- chartが想定している差分はvaluesに書く
- chartが想定していない差分はpatchで矯正する
- patchがchartの設計と喧嘩し始めたら、chartを使い続ける価値を見直す
patchによる矯正は強力です。chartに起因する障害の恒久対策として、不要なhookアノテーションを除去するといったことまでできます。
ただし、それは同時に、chartを更新するたびに、自分のpatchがまだ正しいかを確認する義務を負うということでもあります。
また、chartをrepoに同梱するvendoringには、古いバージョンが残り続ける問題があります。私のrepoでは、最も多いものが6バージョンまで積み上がっていました。
「追加する手順」だけでなく、いつ古いchartを消すかまで最初に決めておく必要があります。
なぜ「KustomizeがHelmを包む」構成にしたのか
前提として、私のクラスタではmanifestをKustomizeのbase/overlay構造で管理し、Argo CDから同期しています。
そこへHelm製品を組み込む方法として、主に次の3つを検討しました。
| 方式 | 利点 | 弱点 |
|---|---|---|
helm install/helm upgradeで管理する |
Helm本来のrelease管理やrollbackを使える | Kustomize中心の管理体系とは別の運用になる |
helm templateの出力をcommitする |
最終的なmanifestを直接読める | 更新のたびに大量の生成差分が出る |
helmChartsとvendoringを使う |
Kustomizeの構造に統合しつつ、valuesとpatchを管理できる | Helm実行環境とpatchの保守が必要 |
私が採用したのは、3つ目の方式です。
各アプリのkustomization.yamlからchartとvaluesを参照し、chart本体もrepoに同梱します。chartとその依存物がローカルにそろっていれば、buildのたびに外部のchart repositoryへ取りに行く必要はありません。
これは、GitOpsにおける「Gitを正本にする」という考え方と相性がよい構成でした。
ただし、chartを同梱しただけで、どの環境でも永久に同じ結果になるわけではありません。
Kustomizeは内部でHelmを使ってchartを展開します。そのため、再現性を高めるには、少なくとも次のものを意識する必要があります。
- chartのバージョン
- chartの依存関係
- values
- patch
- Kustomizeのバージョン
- Helmのバージョン
特にCIとArgo CDで使うKustomizeやHelmのバージョンが異なると、ローカルでは成功するのにArgo CDでは失敗する、といった差が生まれます。
そのため、vendoringは「chartを保存すれば完成」ではなく、manifestを生成するtoolchainも含めて管理する運用だと考えています。
基準1: chartが想定する差分はvaluesに書く
replica数、resource request/limit、Ingressの有効化、永続化、Serviceの形式など、chartのvaluesに設定項目が用意されているものは、原則としてvaluesで変更します。
当たり前に聞こえますが、実際には「valuesの深い階層を探すより、Deploymentをpatchした方が早い」という誘惑があります。
しかし、chartが正式に提供している設定項目をpatchで変更すると、chart内部のリソース名や構造に不必要に依存することになります。
次のバージョンでテンプレートの構造が変われば、patchが適用できずエラーになるか、さらに悪い場合には、エラーにならないまま意図した場所に当たらなくなる可能性があります。
私が判断するときの原則は、次の一文です。
valuesはchartとの公式な契約であり、patchは外側から行う非公式な上書きである。
公式な設定項目で実現できるものを、わざわざ非公式な方法で変更しない。
これが基準1です。
valuesに設定項目が見つからない場合も、すぐにpatchを書くのではなく、まず次の点を確認します。
- valuesの別階層に設定がないか
extraEnvやextraVolumesなどの拡張口がないか- chartの新しいバージョンで対応されていないか
- upstreamに設定例がないか
patchは便利ですが、最初から使うのではなく、chartが用意した拡張口を確認した後に使う方が、長期的な保守は楽になります。
基準2: chartが想定しない差分はpatchで矯正する
一方で、valuesでは届かない場所も確実に存在します。
私の環境でpatchが必要になったのは、たとえば次のようなケースです。
クラスタ固有のストレージ構成
特定ノードに接続したlocal PersistentVolumeを利用する場合、node affinityやvolumeとの対応など、chartが一般的な設定項目として持っていない差分が必要になることがあります。
これはchartの不備ではありません。
chartが想定していない、利用者固有のクラスタ構成だからです。このような差分はKustomizeのpatchで補うのが自然です。
chartに起因する障害への恒久対策
ある認証基盤では、常時存在してほしいリソースにHelm hook関連のアノテーションが付いていました。
そのリソースの扱いとArgo CDの同期が噛み合わず、更新時にサービス全体へ影響する障害が発生しました。
upstreamの修正を待つだけでは、次の更新でも同じ問題を踏む可能性があります。そこで、Kustomizeのpatchを使い、問題となるhookアノテーションを生成後のmanifestから除去しました。
これは、KustomizeでHelmを包む構成の大きな利点です。
chartをforkしなくても、chartの生成結果を自分の環境に合わせて矯正できます。
ただし、強力だからこそ注意が必要です。
chartが修正され、対象のアノテーションがなくなった後もpatchだけが残れば、そのpatchは不要なものになります。対象リソースの名前や構造が変われば、patch自体が失敗する可能性もあります。
chartが用意していないセキュリティ設定
chartによっては、必要なsecurityContext、ラベル、アノテーション、NetworkPolicyとの接続に必要な情報などを十分に設定できない場合があります。
そのような項目を一律に底上げするときも、patchが役立ちます。
ただし、securityContextを設定したことでコンテナが起動しなくなる場合もあります。単に「安全そうな設定を追加する」のではなく、イメージがどのUIDで動くか、書き込みが必要なディレクトリはどこかまで確認する必要があります。
patchは「借金」として管理する
patchを書くこと自体は悪くありません。
問題は、なぜ存在するのか分からないpatchが残ることです。
半年後に次のようなpatchを見ても、その理由が分からなければ、削除してよいのか、更新後も必要なのか判断できません。
そのため最近は、patchそのもの、kustomization.yaml、または近接するドキュメントに、少なくとも次の情報を残すようにしています。
- なぜこのpatchが必要なのか
- 何を回避または実現しているのか
- どのchartバージョンで確認したのか
- upstreamで解消されたら削除できるのか
過去に作ったすべてのpatchで徹底できているわけではありません。実際、理由が書かれていない古いpatchを見て、Gitの履歴から目的を調べ直したことがあります。
だからこそ、patchは単なる設定ファイルではなく、将来確認しなければならない借金として扱うようになりました。
基準3: patchがchartの設計と喧嘩し始めたら、chartを見直す
patchの枚数だけで、chartをやめるべきかは決まりません。
1枚でもchartの中核部分を丸ごと置き換えるpatchなら保守は重くなります。一方、単純なラベル追加であれば、数枚あっても大きな問題にはなりません。
それでも私の感覚では、patchが5枚前後まで増えた段階を、構成を見直すための警告として扱っています。
これは一般的な基準ではなく、あくまで私自身の運用上の目安です。
特に次のような状態になったら、chartを使い続ける価値を再評価します。
- DeploymentやStatefulSetの主要部分をpatchで置き換えている
- コンテナ配列の番号など、chart内部の実装に強く依存している
- chartが作るリソースの多くを削除している
- 複数のサブチャートを無効化し、ほとんど使っていない
- chart更新のたびにpatchを大きく書き換えている
- 生成結果をbuildしないと、最終構成を誰も説明できない
判断するときに見るのは、主に次の2点です。
chartの更新に追従する価値があるか
chartを使う利点の1つは、upstreamが追加した設定、Kubernetes APIの変更、推奨構成などを更新によって取り込めることです。
しかし、構成が単純で更新頻度も低いアプリの場合、chart更新によって得られる価値より、patchを追従させるコストの方が大きくなることがあります。
その場合は、一度helm templateで生成したmanifestを取り込み、通常のKustomize resourceとして管理した方が、最終構成を読みやすくできる可能性があります。
chartの設計思想と自分の使い方が合っているか
hookを多用する、複数のサブチャートをまとめて導入する、独自のライフサイクル管理を前提とする。
chartにとっては便利な機能でも、自分のGitOps環境では不要だったり、障害要因になったりすることがあります。
chartが提供する「便利な仕組み」をほぼ無効化し、その上から大量のpatchを当てているなら、それはchartを採用しているというより、chartと戦っている状態です。
その段階では、chartの利用を続けること自体を見直すべきです。
なお、私自身は、現時点ではchartを完全に捨てて素のmanifestへ移行したところまでは実行していません。
そのため、この基準3は成功事例の紹介ではなく、今後chartから撤退するかを判断するために置いている基準です。
実際に移行する場合は、まず構成が単純なアプリを対象に、次の2案を比較するつもりです。
- chart、values、patchを維持する
- 生成済みmanifestをKustomizeのbaseとして管理する
最終的なファイル数だけでなく、アップデート時のdiff、レビューのしやすさ、upstream変更の取り込みやすさまで比較する必要があります。
chart更新時に確認すること
chartを更新するときは、version番号を変更してbuildが通れば終わり、とは考えないようにしています。
最低限、次の点を確認します。
- valuesのキーが廃止または変更されていないか
- 自分のpatchがすべて適用されているか
- patch対象のリソース名や構造が変わっていないか
- 新しく追加されたリソースがないか
- RBACやCRDに意図しない変更がないか
- DeploymentやStatefulSetの更新方法が変わっていないか
- build結果のdiffが説明できる範囲か
patchが適用できなければbuild時に失敗することもあります。しかし、すべての問題が明確なエラーになるとは限りません。
たとえば、chart側の変更によって同じ設定が別のリソースへ移動した場合、古いpatchが適用できたとしても、期待した効果を持たなくなっている可能性があります。
そのため、patchファイルだけでなく、生成された最終manifestのdiffを見ることが重要です。
valuesは契約、patchは借金ですが、最後にクラスタへ適用されるのは、その両方を合成したmanifestです。
失敗談: 同じchartが6バージョン積もった
vendoringの運用で、最初に決めていなかったことが1つあります。
古いchartをいつ消すかです。
バージョンアップ時には、新しいchartディレクトリを追加します。問題が起きたときに戻せるよう、動作確認が終わるまでは古いchartも残します。
ここまでは合理的です。
しかし、「動作確認が終わったら削除する」という工程を作業手順に入れていませんでした。
その結果、最も多いアプリでは同じchartが6バージョン、そのほかにも5バージョン、4バージョンと残っていました。repoのリファクタリング計画を作るために数えて、初めて気づきました。
実害は、単にrepoの容量が増えることだけではありません。
より問題だったのは、ディレクトリを見ただけでは、現在どのバージョンを使っているのか分かりにくくなったことです。
kustomization.yamlを読めば現在のバージョンは確認できます。それでも、使われていない古いディレクトリが大量に並んでいる状態は、調査時のノイズになります。
また、古いchartを検索結果から開いてしまい、現在の設定だと思い込む可能性もあります。
現在は、新しい運用ルールとして次の流れを採用することにしました。
- 新しいchartを追加する
- ローカルとCIでbuild結果を確認する
- Argo CDで本番同期を確認する
- 問題がなければ旧chartを削除する
- 削除までを同じ更新作業として扱う
ただし、これは今後の更新で適用するために決めたルールです。
過去に残ったchartの整理は、まだリファクタリングの途中です。「すでに完全に徹底できている運用」として書ける状態ではありません。
vendoringを始めるなら、追加方法と同時に、削除条件も決めておくことをおすすめします。
向いている人・向かない人
この構成が向いている人
- すでにKustomize中心のGitOps運用がある
- Helm製品も同じbase/overlay構造で扱いたい
- chartの生成結果へKustomizeのpatchを適用したい
- chart repositoryへの依存を減らしたい
- 過去のcommitから構成を再現しやすくしたい
- valuesとクラスタ固有の差分を分離したい
この構成が向かない人
helm upgradeやhelm rollbackをそのまま使いたい- Helm releaseを運用単位として管理したい
- KustomizeやHelmのbuild環境まで管理したくない
- chartの生成後にpatchを当てる仕組みを複雑に感じる
- Argo CDからHelm chartを直接管理する方が分かりやすい
KustomizeがHelmを包む構成では、Helmは主にchartからmanifestを生成するために使われます。
実際のライフサイクル管理を担当するのはArgo CDであり、Helmのrelease管理機能ではありません。
Helmのrelease管理に価値を感じるなら、無理にKustomizeへ統合せず、Helmを正として運用する方が素直です。
まとめ
HelmかKustomizeかは、単純な二者択一ではありませんでした。
私の環境では、Helm chartをKustomizeの中へ取り込み、差分の性質によってvaluesとpatchを使い分ける形に落ち着きました。
判断基準を一言ずつにすると、次のようになります。
- valuesは契約
- patchは借金
- 借金がchartの価値を上回ったら、関係を見直す
chartの設定項目で表現できるものはvaluesへ書く。
chartが想定していないクラスタ固有の差分はpatchで補う。
そして、patchがchartの中核構造に依存し始めたら、「どうpatchするか」ではなく、chartを使い続ける必要があるかを考える。
この3段階を意識するようになってから、chartのバージョンアップは、動くことを祈る作業ではなく、確認項目を順番に潰す作業へ変わりました。
vendoringについても同じです。
chartをrepoへ追加するだけでは、運用は完成しません。古いchartをいつ削除するか、toolchainをどうそろえるか、patchの理由をどこへ残すかまで決めて、初めて長く維持できる構成になります。
次に読む記事
- クラスタ内Gitの落とし穴|k3s GitOpsを全損から復旧する設計 ― chartを含む構成一式をGitから再現するための考え方
- 実際に試したセルフホストソフトウェア一覧【2026年版】 ― 実際に運用したセルフホスト製品と、継続利用の判断