💧

kptでWETなKubernetesマニフェスト管理

2024/09/25に公開

kptでWETなKubernetesマニフェスト管理

みなさん、Kubernetesでのマニフェスト管理ってどうしてますか?
Kubernetesでは、GitOpsと呼ばれる運用方法を用いてマニフェストをGitで管理することが一般的です。
マニフェストをGitで管理することで、変更履歴の追跡、コードレビュー、環境ごとのリリース管理をおこなうことができます。
一方でWall of YAMLとも呼ばれるように、マニフェスト管理は一筋縄ではいかず、Kubernetesの利用者を悩ませる課題の1つとなっています。

本記事では、メジャーなマニフェスト管理ツールの課題やマニフェストのリリース方式の課題、WETリポジトリと呼ばれるマニフェストの管理方法を紹介した後、
kptと呼ばれるツールを利用し、既存ツールの課題やリリース方式の問題点を解決する方法について解説します。

マニフェスト管理ツール

Kubernetesのマニフェストを管理する場合、そのマニフェストをYAML形式で直書きすることはあまり多くありません。
実運用では、ステージング環境と本番環境で設定が微妙に異なったり、アップストリームから取得したマニフェストをカスタマイズして利用したり、複数のマニフェストを一括で生成する必要がでてきます。
そのような要件を満たすために、マニフェストのテンプレート化やパッケージ化、パッチの適用、プログラマブルな生成処理などが可能なマニフェスト管理ツールを利用することが一般的です。

では、Kubernetesのマニフェスト管理ツールはどれを使えばいいのでしょうか?
以下の記事では、2024年現在よく利用されているマニフェスト管理ツールが紹介されています。

https://itnext.io/kubernetes-configuration-in-2024-434abc7a5a1b

KustomizeとHelmがもっともよく利用されていますが、それ以外にも非常にたくさんのマニフェスト管理ツールが登場していることがわかります。

ここでは代表的なマニフェスト管理ツールであるKustomizeとHelmに、我々がよく利用しているJsonnetを加えた3つのツールについて簡単に紹介します。(代表的なGitOpsツールであるArgo CDでも、この3つのツールが標準でサポートされています)

Kustomzieは、Kubernetes向けのマニフェスト管理ツールで、YAML形式のマニフェストファイルに対してパッチを適用して、新しいマニフェストを生成することができます。
パッチを適用することでどんなフィールドでも書き換えることができるので、非常に柔軟です。
このような特徴から、あるOSSが提供しているマニフェストをカスタマイズして利用する場合に向いています(このような利用方式をoff-the-shelf configurationと呼びます)。
また、共通のベースとなるマニフェストに対して、環境ごと(例:QA環境、開発環境、ステージング環境、本番環境など)の差分をパッチとして適用するオーバーレイ方式でのマニフェスト管理も得意としています。
一方で、テンプレートによる記述はサポートしておらず、プログラマブルな生成処理を書くこともできません。

次にHelmは、Kubernetesのマニフェストをパッケージ管理するためのツールです。
なんらかのOSSを導入するためのマニフェスト群をパッケージングして提供することができます。
多くのOSSはHelm Chartと呼ばれるパッケージを提供しており、簡単に導入することができます。
また、Helmはテンプレート形式でマニフェストを記述することができ、パラメータ化された箇所に設定を埋め込んだり、条件分岐や繰り返し処理を記述することもできます。
一方で、テンプレートのパラメータになっていない箇所を書き換えることはできません。
また、テンプレート化されたマニフェストはYAML形式ではなくなってしまうため、ツールでのサポートが受けられなかったり、可読性が低くなる場合があります。

Jsonnetは、JSONを拡張した言語で、変数や関数や条件分岐やループなどの機能を利用して、柔軟にJSONを生成することができます。
柔軟性が非常に高く、どんな複雑な生成処理でも書くことができるでしょう。
一方で、独自の言語記法を覚える必要があり、慣れるまでは可読性や書きやすさが低いと感じるかもしれません。
また、既存のYAML形式のマニフェストをカスタマイズして利用する場合には、Jsonnetで書き直す必要があるため、手間がかかるかもしれません。

このようにマニフェスト管理ツールにはそれぞれ特徴があり、チームの運用スタイルや要件に合わせて選択する必要があります。

マニフェストのリリース管理

GitOpsでマニフェストを管理する場合、利用するツールだけでなく、リリースの管理方法も重要です。
マニフェストをリリースするとき、通常は1つの環境だけではなく、複数の環境に適用することになります。
例えば、テストのためにQA環境に適用して、問題がないことが確認できたらステージング環境に適用し、しばらく問題がないことを確認してから本番環境に適用するといった流れです。
このとき、各環境ごとに適用するマニフェストはそれぞれ異なりますし、リリースするタイミングも違います。

そこで、Gitのブランチを使ってリリースするマニフェストを管理する方法がよく使われます。

  1. mainブランチからfeatureブランチを切ってPRを作成する。
  2. PRが完成したら、mainブランチにマージする。
  3. mainブランチをstageブランチにマージする。stageブランチの内容がステージング環境に適用される。
  4. stageブランチをprodブランチにマージする。prodブランチの内容が本番環境にマージされる。

しかし、ブランチ方式の場合、マージやリバートの際にコンフリクトが発生するなどの問題が発生することがあります。
ブランチ方式の問題点については、詳しくは以下の記事を参照してください。

https://codefresh.io/blog/stop-using-branches-deploying-different-gitops-environments/

一方、Kustomizeでは環境ごとにオーバーレイディレクトリを作成して、ベースとなるマニフェストにパッチを適用することで、環境ごとのマニフェストを生成することができます。

├── base
│    ├── deployment.yaml
│    └── kustomization.yaml
└── overlays
     ├── prod
     │    ├── kustomization.yaml
     │    └── patch.yaml
     └── stage
          ├── kustomization.yaml
          └── patch.yaml

しかしKustomizeの場合、baseのマニフェストを変更するとすべての環境のマニフェストが変更されてしまうため、リリースのタイミングを制御しにくいという問題があります。
そこで、オーバーレイディレクトリを分離した上で、リリースのタイミングはブランチで制御するという方法が用いられることがあります。

Googleのベストプラクティスにおいても、ブランチではなくフォルダを使って環境ごとのマニフェストを管理する方法が推奨されています。

https://cloud.google.com/kubernetes-engine/enterprise/config-sync/docs/concepts/gitops-best-practices#use-folders

DRYかWETか

プログラミングの世界ではDRY (Don't Repeat Yourself) 原則という言葉がよく使われます。
これは、同じ処理を何度も書くのではなく、共通化やモジュール化をすることで重複を避けるべきだという考え方です。
一方、この反対の考え方としてWET (Write Everything Twice) という言葉があります。

マニフェスト管理におけるWETとは、マニフェスト管理ツールによってレンダリングされたマニフェストをGitリポジトリに保存して管理する方式です。
一般的にプログラミングの世界ではWETはアンチパターンとされていますが、マニフェスト管理においてはWETな管理が有効な場合もあります。

WETリポジトリによるマニフェスト管理には以下のようなメリットがあると考えられます。

  • 分かりやすい
    • 各環境にどのようなマニフェストがデプロイされているのか一目で分かる。
    • GitHubのPRを作成した際に、変更内容が分かりやすい。
    • ディレクトリ単位でDiffを取ることで、各環境間の差分を簡単に確認できる。
  • CDツールでの対応が不要
    • Argo CDでは、Kustomize, Helm, Jsonnet以外のマニフェスト管理ツールを利用する場合、プラグインを作成する必要がある。
      WETリポジトリ方式ではYAML形式のマニフェストがGitリポジトリにそのまま登録されているので、CDツール側での対応が不要となる。

Googleのベストプラクティスにおいても、WETリポジトリの作成が推奨されています。(DRYからWETにすることをHydrate(水分を与える)と呼んでいます。ちょっとおしゃれな言い回しですね)

https://cloud.google.com/anthos-config-management/docs/concepts/gitops-best-practices#create-wet-repo

また、Argo CDではmanifest hydratorという仕組みが提案されています。
この機能は、Argo CDがマニフェストをレンダリングした結果をリポジトリにPushしてくれる仕組みのようです。

https://github.com/argoproj/argo-cd/pull/19066

kpt

ここまで解説してきたように、マニフェスト管理には様々な課題があります。
これらの課題を解決するために、kptというマニフェスト管理ツールを紹介したいと思います。

https://kpt.dev

kptは以下のような特徴を備えています。

  • Helmのようにパッケージ管理ができる。Gitリポジトリで管理されている生YAMLや、Helm Chartもkptのパッケージとして扱うことができる。
  • Helmのようにテンプレート的な記述ができる。ただし、条件分岐や繰り返しは書けないので、可読性の低いテンプレートは生まれにくい。
  • Kustomizeのように、off-the-shelf configurationの管理ができる。Upstreamのマニフェストを自由にカスタマイズできる。
  • Jsonnetのようにプログラマブルな生成処理を書くことができる。(GoとTypeScriptに加え、Pythonの方言であるStarlarkで記述することが可能)
  • WETリポジトリの管理に適している。(後述)

自前パッケージの管理

ここでは、kptで自前のマニフェストを管理する方法を解説します。
以下のコマンドを実行して、パッケージのテンプレートを作成します。

$ cd /path/to/your/repo
$ mkdir -p packages/nginx
$ cd packages
$ kpt pkg init nginx

作成に成功すると、以下のように3つのファイルが生成されます。
Kptfileはパッケージに関するメタデータを記述するためのファイルです。
package-context.yamlは、kptでマニフェストのレンダリングをおこなう際に利用する設定を記述するためのファイルです。

└── packages
    └── nginx
        ├── Kptfile
        ├── package-context.yaml
        └── README.md        

作成したディレクトリに、nginxのDeploymentのマニフェストを保存します。

packages/nginx/nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    component: nginx
spec:
  replicas: 1 # kpt-set ${replicas}
  selector:
    matchLabels:
      component: nginx
  template:
    metadata:
      labels:
        component: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.20

ここで、spec.replicasに# kpt-set ${replicas}というコメントを入れておきます。
このマーカーを付けることによって、後ほどパラメーターを設定することができます。

次に、setter-config.yamlを用意します。
これはHelmのvalues.yamlのようなもので、パラメータを記述するためのファイルです。

packages/nginx/setter-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: setter-config
  annotations:
    config.kubernetes.io/local-config: "true"
data:
  replicas: "3"  

Kptfileを編集して、apply-setters functionを実行するようにします。
apply-setters functionは、setter-config.yamlに記述されたパラメータをマニフェストに適用するためのfunctionです。

packages/nginx/Kptfile
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
  name: nginx
  annotations:
    config.kubernetes.io/local-config: "true"
info:
  description: nginx
pipeline:
  mutators:
    - image: gcr.io/kpt-fn/apply-setters:v0.2.0
      configPath: setter-config.yaml  

最後に、マニフェストのレンダリングをおこないます。

$ cd /path/to/your/repo/packages/nginx
$ kpt fn render

これにより、setter-config.yamlに記述したパラメータがマニフェストに適用されているでしょう。

作成したパッケージのディレクトリ構成は以下のようになります。

└── packages
    └── nginx
        ├── Kptfile
        ├── nginx-deployment.yaml
        ├── package-context.yaml
        ├── setter-config.yaml
        └── README.md        

上記の変更をmainブランチにマージした後、タグを打ちます。

$ git tag packages/nginx/v1
$ git push origin packages/nginx/v1

以上でパッケージを作成することができました。

次にこのパッケージを利用して、各環境ごとにカスタマイズされたマニフェストを生成する方法を解説します。

$ cd /path/to/your/repo
$ mkdir -p manifests/production
$ cd manifests/production
$ kpt pkg get git@github.com:your-org/your-repo.git/packages/nginx@v1

manifestsディレクトリは、Hydrateされたマニフェストを保存するためのディレクトリです。
productionやstagingなど、各環境ごとにディレクトリを作成して、そこにHydrateされたマニフェストを保存します。
kpt pkg getコマンドを使って、さきほど作成したパッケージを取り込んでいます。

ディレクトリ構成は以下のようになります。

├── manifests
│   └── production
│       └── nginx
│           ├── Kptfile
│           ├── nginx-deployment.yaml
│           ├── package-context.yaml
│           ├── setter-config.yaml
│           └── README.md        
└── packages
    └── nginx
        ├── Kptfile
        ├── nginx-deployment.yaml
        ├── package-context.yaml
        ├── setter-config.yaml
        └── README.md        

manifestsディレクトリ以下のマニフェストは自由にカスタマイズすることができます。
kptのfunctionsを使って書き換えるだけでなく、YAMLファイルを直接編集しても構いません。
これは、Kptfileでパッケージの取得元のリビジョンを記録しており、変更履歴を追跡できるようになっているためです。

次にKptfileを編集して、set-labels functionを利用してラベルを付与します。

manifests/production/nginx/Kptfile
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
  name: nginx
  annotations:
    config.kubernetes.io/local-config: "true"
info:
  description: nginx
pipeline:
  mutators:
    - image: gcr.io/kpt-fn/apply-setters:v0.2.0
      configPath: setter-config.yaml
    - image: gcr.io/kpt-fn/set-labels:v0.1.5
      configMap:
        env: production
      exclude:
        - kind: Kptfile

また、setter-config.yamlを編集して、replicasを5に変更します。

manifests/production/nginx/setter-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: apply-setters-fn-config
  annotations:
    config.kubernetes.io/local-config: "true"
data:
  replicas: "5"  

マニフェストをレンダリングします。

$ cd /path/to/your/repo/manifests/production/nginx
$ kpt fn render

これで、production環境に適用するマニフェストが完成しました。
あとは、Argo CDなどのGitOpsツールを使って、このマニフェストを環境に適用するだけです。

なお、Argo CDを利用してマニフェストを適用する場合、KptfileがKubernetes環境にデプロイされてしまわないように、以下のようにargocd.argoproj.io/hook: Skipアノテーションを付けておく必要があります。

Kptfile
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
  name: sample
  annotations:
    config.kubernetes.io/local-config: "true"
    argocd.argoproj.io/hook: Skip

パッケージの更新

ここでは、自作したパッケージを更新する方法について解説します。

まず、packages/nginx/ディレクトリにあるファイルを更新し、kpt fn renderコマンドを実行してマニフェストをレンダリングします。
変更をmainブランチにPushした後、バージョンv2としてタグを打ちます。

$ git tag packages/nginx/v2
$ git push origin packages/nginx/v2

続いてパッケージの利用側で、kpt pkg updateコマンドを利用して更新作業を実施します。

$ cd /path/to/your/repo/manifets/production/
$ kpt pkg update nginx@v2

kptでは3-way merge方式を採用しており、ローカルのパッケージ(local)、アップデート前のパッケージ(origin)、アップデート後のパッケージ(upstream) の3つの状態を比較してマージをおこないます。

例えば、originには存在するがupstreamには存在しないリソースは削除されますが、originとupstreamには存在せずlocalにのみ存在するリソースはそのまま保持されます。
このような仕組みにより、ローカルのマニフェストに変更が加えられていたとしても適切にアップデートが実施されます。

ただし、変更内容によってはマージ処理がうまくいかない場合があります。
その場合は、kpt pkg update--strategy force-delete-replaceオプションを指定したり、kpt pkg diffで差分をチェックして手動でマージをおこなう必要があります。

kpt pkg updateに関する詳細は以下のドキュメントを参照してください。

最後にレンダリングをおこない、変更をPushしたら更新作業の完了です。

$ cd /path/to/your/repo/manifets/production/nginx
$ kpt fn render

Helmパッケージを管理する

kptはHelmと同じようにパッケージ管理の機能を備えています。
しかし、世の中の多くのOSSはHelm Chart形式でマニフェストのパッケージを提供しています。

kptは、Gitリポジトリで管理されているYAMLファイルや、Helm Chartもパッケージとして扱うことができます。
ここではkptを使ってHelm Chartを管理する方法について、cert-managerを例に解説します。

自前パッケージを作成するときと同じようにパッケージのテンプレートを作成します。

$ cd /path/to/your/repo
$ mkdir -p packages/cert-manager
$ cd packages
$ kpt pkg init cert-manager

Kptfileを編集し、render-helm-chart functionの設定を追加します。

packages/cert-manager/Kptfile
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
  name: cert-manager
  annotations:
    config.kubernetes.io/local-config: "true"
info:
  description: Package of cert-manager manifests
pipeline:
  mutators:
  - image: gcr.io/kpt-fn/render-helm-chart:v0.2.2
    configPath: cert-manager-chart.yaml

続いて、Helm Chartに関する設定を記述したファイルを作成します。

packages/cert-manager/cert-manager-chart.yaml
apiVersion: fn.kpt.dev/v1alpha1
kind: RenderHelmChart
metadata:
  name: cert-manager
  annotations:
    config.kubernetes.io/local-config: "true"
helmCharts:
- chartArgs:
    name: cert-manager
    version: v1.15.3
    repo: https://charts.jetstack.io
  templateOptions:
    releaseName: cert-managerr
    namespace: cert-manager
    values:
      valuesInline:
        crds:
          enabled: true

最後にrenderします。
このとき、render-helm-chartはネットワークアクセスするので、--allow-networkオプションを付ける必要があります。

kpt fn render --allow-network

作成したパッケージをmainブランチにマージしてタグを打ちます。

$ git tag packages/cert-manager/v1
$ git push origin packages/cert-manager/v1

後は自前パッケージと同様に、manifestsディレクトリにパッケージを取り込んでカスタマイズすることができます。
このパッケージを利用すると、Helm Chartのvaluesを利用してカスタマイズできるのはもちろんのこと、kptのapply-settersを使ってHelm Chartではパラメータ化されていない箇所を編集することもできます。

なお、以下のsource-helm-chart functionを利用すると、Helm Chartのtar-ballをRenderHelmChartに埋め込むことができ、render時にネットワークアクセスを回避することができます。
公式のfunctionではありませんが、興味があれば試してみるとよいでしょう。

https://github.com/krm-functions/catalog/blob/main/docs/render-helm-chart.md

Starlarkによるカスタマイズ

ここまで紹介してきたマニフェストの変更作業は、一部のフィールドを書き換えるだけの単純なものでした。
しかし、実際の運用ではもっと複雑なマニフェストの操作が必要になることがあります。

kptでは、GoやTypeScriptを使って新たなfunctionを作ることもできますし、Starlarkを利用してマニフェストを操作することもできます。

例えば、paramsで指定した名前と一致するDeploymentリソースを削除する処理をStarlarkで書いてみます。

remove-deployment.yaml
apiVersion: fn.kpt.dev/v1alpha1
kind: StarlarkRun
metadata:
  name: remove-deployments
  annotations:
    config.kubernetes.io/local-config: "true"
params:
  targets:
    - nginx
    - mysql
source: |
  result = []
  for resource in ctx.resource_list["items"]:
    remove = False
    if resource["kind"] != "Deployment":
      result.append(resource)
      continue
    for target in ctx.resource_list["functionConfig"]["params"]["targets"]:
      if resource.get("metadata", {}).get("name", "") == target:
        remove = True
        break
    if remove == False:
      result.append(resource)

  ctx.resource_list["items"] = result

これを利用するには、KptfileにStarlark functionの設定を追加します。

Kptfile
pipeline:
  mutators:
    - image: gcr.io/kpt-fn/starlark:v0.5.0
      configPath: remove-deployments.yaml

このように、Starlarkを利用すると柔軟にマニフェストを操作することができます。
なお、似たような変更操作を何度もおこなう場合は、GoやTypeScriptでfunctionを作成し、再利用できるようにコンテナイメージ化しておくのがおすすめです。

CIによる補助

これまでに紹介してきたkptの利用方法を見てみると、マニフェストのレンダリングをしたり、パッケージの変更の度にGitのタグを打つ必要があったり、手作業が多いことがわかります。
こういった操作は作業漏れやミスが発生しやすいため、CIで自動化したり、補助ツールを作成することが望ましいです。

例えば、我々は以下のような仕組みを導入しています。

  • Gitリポジトリにpushした際に、CIでkpt fn renderの実行漏れをチェックする
  • パッケージをmainブランチにマージしたときに自動的にタグを打つ
  • 新しいパッケージが公開された後、パッケージの利用側で更新忘れをチェックするためのスクリプトを用意

まとめ

本記事では、kptを使ったマニフェスト管理方法について解説しました。
kptはパッケージ管理機能、テンプレート機能、プログラマブルな生成処理などを備えており、他のマニフェスト管理ツールと比較しても遜色なく、柔軟にマニフェストを管理することができます。
さらにWETリポジトリ方式を導入することで、環境ごとのマニフェストの差分が分かりやすくなり、リリースのフローがシンプルになったと感じています。

本記事が読者の皆様のマニフェスト管理に少しでも参考になれば幸いです。

参考

サイボウズ Necoチーム 😺

Discussion