環境差を扱う二つの文法
一言でいうと
devとprodの違いをYAMLのコピーで表現すると必ず分かれてしまいますが、Kustomizeのオーバーレイ(overlay)やHelmの値ファイルで表現すれば、違いだけがファイルとして残ります。
なぜ必要なのか
同じアプリを3つの環境にデプロイするとします。違うのは、replicas、ログレベル、イメージのタグの3つだけです。このときdeployment-dev.yamlとdeployment-prod.yamlをそれぞれ作れば、最初の1か月は楽です。しかし半年たつと、2つのファイルは互いに別の生き物になります。prodにだけ追加したセキュリティ設定がdevにはなく、devで直したプローブの設定がprodには反映されません。どちらが正解なのか誰にもわからない状態になった瞬間に、「devでは動いたんですが」という一言が始まります。
解決の方向は同じです。共通部分は1回だけ書き、違いだけを別に書くのです。Kustomizeはこれを「ベースにパッチを重ねる」と解き、Helmは「テンプレートに値を注入する」と解きます。
どう動くのか
Kustomizeの単位はkustomization.yamlです。ベースには完全なマニフェストとそれをまとめる指示書があり、オーバーレイはベースをresourcesで取り込んで、変形だけを書きます。
| フィールド | すること |
|---|---|
resources |
何を取り込むか(ファイル、または別のkustomizationディレクトリ) |
namePrefix / nameSuffix |
名前の前後に付ける文字列 |
namespace |
すべてのリソースのネームスペースを一括指定 |
labels(旧commonLabels) |
すべてのリソースに共通のラベルを付ける |
patches |
戦略的マージパッチで特定のフィールドだけを上書きする |
images |
イメージの名前/タグを差し替える |
configMapGenerator |
ファイル・リテラルからConfigMapを生成する |
重要な性質が2つあります。1つ目は、kustomize buildの結果にはKustomization自体が入っていないことです。それは成果物ではなく指示書だからです。ビルド結果にKustomizationが混ざって出てくるなら、どこかで指示書をresourcesとして誤って取り込んでいます。
2つ目は、configMapGeneratorが作ったConfigMapの名前の末尾に、内容のハッシュが付くことです。app-config-9b2f4kt6mdのような名前になり、そのConfigMapを参照するDeploymentの参照名も自動で一緒に変わります。この設計のおかげで、設定ファイルを直すとPodテンプレートが変わり、ロールアウトがひとりでに起きます。ハッシュがないと、ConfigMapだけが変わってPodは古い設定を持ったまま動き続けるという、原因の特定が最も難しい種類の事故が起きます。
Helmはアプローチが違います。テンプレートにvalues.yamlを注入してマニフェストを作り、環境の違いはvalues-prod.yamlのような値ファイルや個別のパラメーターで与えます。Argo CDでは、この2つがspec.source.helm.valueFilesとspec.source.helm.parametersに対応します。ここで必ず知っておくべきことが1つあります。Argo CDはhelm installをしません。repo-serverがhelm templateでマニフェストをレンダリングしてから、それをapplyします。そのため、クラスターでhelm listをしても何も出てこず、ロールバックもHelmのリビジョンではなくgitのコミットで行います。
現場での姿
1つ目は、ベースを直してdevに合わせてしまうミスです。devのreplicasを1にしたくてベースを1に直すと、prodまで1になります。ベースはすべての環境の共通項でなければならず、環境固有の値は必ずオーバーレイにある必要があります。
2つ目は、targetRevision: HEADの落とし穴です。ブランチの最新に追従すると楽ですが、同じApplicationの定義が時点ごとに別のものをデプロイします。再現可能なデプロイを望むなら、タグやコミットハッシュで固定します。:latestイメージタグを禁止するのと、まったく同じ理由です。
3つ目は、名前の接頭辞とパッチの対象です。ベースにnamePrefixが付いていると、ビルド結果の名前はlabhub-webになりますが、パッチはベースに書かれた元の名前で対象を探します。このルールを知らないと、「パッチが適用されない」でかなり迷います。
2つを一緒に使うときに決めておくこと
Argo CDは、Helmチャートをレンダリングしたあとで、kustomizeを重ねられます。便利ですが、どこまでをどのツールが担当するかを決めておかないと、誰も最終結果を予測できません。
基準は単純です。値で表現できるものはHelmのvaluesで、値で表現できないもの(サイドカーの追加、特定フィールドのパッチ、リソースを1つ足すこと)は、kustomizeのパッチで行きます。Helmがすでに公開している値をkustomizeで上書きすると、最終的な値を知るために2か所を見なければなりません。
レンダリング結果を目で見てからコミットします。パイプラインで実際に適用されるYAMLを作ってレビューに載せれば、レビュアーは値ファイルではなく、結果を見ます。
helm template app ./chart -f values-prod.yaml \
| kustomize cfg cat > rendered/prod.yaml
この方式(レンダリングされたマニフェストをgitに置くこと)は、リポジトリが大きくなる代わりに、何が変わるかがdiffにそのまま見えます。GitOpsの利点の半分は、ここから生まれます。
Argo CDにレンダリングを任せるときは、バージョンを固定します。Helmとkustomizeのバージョンが変わると、レンダリング結果が変わることがあります。Argo CD側のバージョンを固定し、CIでも同じバージョンでレンダリングして照合します。
namespaceを2か所で決めません。チャートのnamespaceの値、kustomizeのnamespace:、Argo CD Applicationのdestination.namespaceが互いに違うと、リソースが散らばります。1か所だけで決めます。
CRDは例外です。Helmのcrds/はアップグレードで更新されず、kustomizeのパッチは、CRDがすでにあって初めてスキーマ検証を通ります。CRDは別のApplicationに分けて、sync-waveを前に置くほうが安定します。
秘密は、どちらにもコミットしません。2つのツールとも、平文をそのままレンダリングします。
次のラボですること
/root/gitops/kustomize/の下にベースとdev・prodのオーバーレイを作り、名前の接頭辞・共通ラベル・ネームスペース・戦略的マージパッチ・configMapGenerator・imagesを1つずつ重ねながら、kustomize buildの結果がどう変わるかをファイルに残します。そのあと、Helmソースを使うApplicationと、Kustomizeのオーバーレイを指すApplicationをそれぞれ書いて、Argo CDが2つの文法をどう受け取るかを、宣言で表現します。