LoginSignup
1
0

Helmチャートのスナップショットテストのすゝめ

Last updated at Posted at 2023-12-12

この記事はKubernetes Advent Calendar 2023の10日目の記事です。

はじめに

Helmチャートのテストと継続的な一貫性の確保は、Helmチャートを作成した経験のある方にとっては永遠についてくるタスクです。新機能やKubernetesのAPIへの変化に対応するためにHelmチャートに新しいプロパティなどを追加する場合など、バージョンアップしてもユーザーが保持するValuesに対して一貫性を維持する必要があります。

Helmチャートを直接作成していない場合でも、他の誰かが作成したHelmチャートを自身のクラスターにインストールするために作成したvaluesファイルがある限り、チャートがアップデートされていく過程で問題なく機能するか、またDeprecateされたプロパティがないか、期待される生成結果に変更がないかを確認する必要があります。

Helmの実行に必要なのは基本的に対象のチャートとValuesだけで、入力されたValuesに対して、マニフェストの全量が出力されます。
Valuesファイルのパターンに応じて出力が異なる部分と、Valuesの組み合わせによりその他の部分に影響がないことを確認する必要があるでしょう。

となると、思いつくのはWebのスナップショットテストです。
helm template YOUR_CHART -f VALUES.yamlの実行結果をスナップショットとしてCIで継続的に取得し、差分を確認していけたらいいなと思いました。

そのためスナップショットが出来るツールをいくつか探したのですが、手軽に実行できるものを見つけることができず、Helmに特化したシンプルなスナップショットテストツールを作成しました。
Helmにはプラグイン機構があり、Helmプラグインとして作成したため、最後にそのナレッジもまとめたいと思います。

既存ツール

Helmチャートのスナップショットテストを行えるツールはすでにいくつかあります。1

  • helm-unittest: Helmのユニットテストツールです。一般的な単体テストのassertによるテストに加えてスナップショットテストの機能もあります。

https://github.com/helm-unittest/helm-unittest#snapshot-testing
helm-unittestでスナップショットテストを行うには下記のようなテストスペックを用意します。

templates:
  - templates/deployment.yaml
tests:
  - it: manifest should match snapshot
    asserts:
      - matchSnapshot: {}
  • terratest: IaCの包括的なテストツールで、Goでテストコードを記載します。Helmの実行結果を扱うパッケージがあります。ただスナップショットを取得するコードは自分で書かないといけなそうです。

調べた限りアクティブなプロジェクトは上記の2つかなと思います。

ただこれらはスナップショットテストに特化したツールではなく、単にhelm template YOUR_CHART -f VALUES.yamlをやりたいだけの私としては、以下の点で求めていた機能が不足していました。

  • 素のValuesファイルが使える: helm template YOUR_CHART -f VALUES.yamlをやりたいだけなので、あくまでも素のValuesファイルを用意するだけで実行できる手軽さが欲しいです。ツール固有のテストスペックファイルを用意したくありません。

  • 生成結果全てを扱う: 確認したいのはテンプレートの一部ではなく、生成結果全てです。
    例えばhelm-unittestはファイルベースの考え方であるため、テストスペックファイルにどのファイルのスナップショットを撮るか指定する必要があり、以下のパターンではうまく機能しませんでした。

よくあるtemplateの構成としては次のような構成があると思います。
例1: ingressのオプションで、kind: Ingressのマニフェストファイルを用意し、ファイル全体をifブロックで囲う。最近だとIngressリソースに加えてGatewayリソースのオプションも追加が必要な場合もあります。
例2: httpsのオプションで、cert-managerを利用するか、Helmの自己証明書生成機能で生成された証明書を使用するか選べる。cert-managerのkind: Certificateのマニフェストファイルを用意し、ファイル全体がifブロックで囲う。

helm-unittestのスナップショットでは、ファイルに対する生成結果が存在しない場合のテストを行うことができません。

  • Helm Functionへの対応: Helmにはランダム値や自己証明書などを動的を生成する機能があります。スナップショットテストにおいては動的に生成された項目のみ対象から除外したいですが、これらのツールは動的な値を扱うことができなかったり、扱う場合にコードを書く必要があります。
    https://helm.sh/docs/chart_template_guide/function_list/#randalphanum-randalpha-randnumeric-and-randascii

  • 他のチャートに対するValuesファイルの確認: 自分で作成したHelmチャートだけでなく、他の人のHelmチャートに対して自分のValuesファイルで生成された結果を保持したいです。既存のツールはあくまで自分で作成したチャートを対象としており、できても手軽には取得できません。

helm-chartsnap

上記のような点からHelmに特化したシンプルなスナップショットテストツールを作成しました。
Helmにはプラグイン機構があるので、Helmプラグインとして作成しています。

実行イメージは下記のとおりです。

Screenshot

インストール

以下のコマンドでHelmプラグインとしてインストールできます。

helm plugin install https://github.com/jlandowner/helm-chartsnap

主な機能

手軽さ

まず、デフォルトのValuesファイルに対してスナップショットをとる場合はチャートを指定するだけです。

# デフォルトのValuesを使用する場合
helm chartsnap -c YOUR_CHART

もちろん特定のValuesファイルに対して実行することもできます。

# 特定のvaluesファイルを使用する場合
helm chartsnap -c YOUR_CHART -f https-enabled-values.yaml

テストケースはあくまで期待するパターンのvalues.yamlを複数用意するだけです。
次の通り、まとめてスナップショットも撮れます。

# 複数のvaluesファイルを配置したディレクトリに対してまとめてスナップショットを取得する
helm chartsnap -c YOUR_CHART -f ./tests/

内部では単にhelm templateを実行しているだけなので、helm templateコマンドの任意のオプションが使えます。
(任意のオプションを渡せるようにパッケージを読み込むのではなく、helmのバイナリを実行するようにしました。)

# helm testをスキップする
helm chartsnap -c YOUR_CHART -f YOUR_TEST_VALUES_FILE -- --skip-tests

# 特定のリソースだけ出力する
chartsnap -c YOUR_CHART -f YOUR_TEST_VALUES_FILE -- --show-only SOMETHING

Jestと同様で、スナップショットファイルがない場合はスナップショットを作成し、スナップショットがあれば差分を出力します。また差分を更新する場合は-uオプションをつけて実行するだけです。

# スナップショットの更新
helm chartsnap -c YOUR_CHART -u

外部のチャートのスナップショットの取得

単にhelm templateを実行しているだけなので、他の人のチャートのスナップショットも取得可能です。

例: ingress-nginxのHelmチャートのスナップショットを取得

# helm templateコマンドの`--repoオプション`を使用することで、事前にリポジトリの追加をすることなく実行できる
helm chartsnap -c ingress-nginx -f YOUR_VALUES_FILE -- --repo https://kubernetes.github.io/ingress-nginx --namespace kube-system --version 4.8.3

例: Hubbleを有効化したciliumのHelmチャートのスナップショットを取得

helm chartsnap -c cilium -- --repo https://helm.cilium.io --namespace kube-system --set hubble.relay.enabled=true --set hubble.ui.enabled=true

例: OCIレジストリに格納されたチャートのスナップショットを取得する

helm chartsnap -c oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric -n nginx-gateway

その他の例はこちらを参照してください。
https://github.com/jlandowner/helm-chartsnap/tree/main/example/remote

Helmに特化した機能

差分箇所の詳細表示

Helm特化のツールであるため、単にスナップショットの差分を表示するのではなく、複数のYAMLで構成された出力結果を読み込んで、どのリソースの何行目の差分かを表示します。

Screenshot

Helm Functionによる動的な出力の取り扱い

Helm Functionを使用して動的な値となる箇所は、values.yamlファイル内にJSONPathで指定することで、実行毎に差分として出力されないようにできます。

# chartsnapの動作変更用のプロパティをValuesファイルに追加する
# この場合、生成された結果のうち`auth-env`という名前のSecretの`.data.SESSI0N_KEY`という項目が固定値に置換されます。
testSpec:
  dynamicFields:
    - apiVersion: v1
      kind: Secret
      name: auth-env
      jsonPath:
        - /data/SESSI0N_KEY

# 他は普通のValuesでOK
# ...

これにより対象箇所は固定値に置換され、スナップショット上で差分として現れてこないようになります。

GitHub Actionも用意しました

継続的にchartsnapを使用してスナップショットテストを実行するためのGitHubアクションも用意しました。これにより継続的にスナップショットを取得し、GitHub上でも差分の確認ができます。

GitHub ActionsのWorkflowファイルの例です。どのチャートにどのValuesファイルを取得して実行するかを指定するだけです🙌

# GitHub Actionでingress-nginxのスナップショットを取得し、差分があればプルリクエストを作成して通知する例
jobs:
  chartsnap-ingress-nginx:
    runs-on: ubuntu-latest
    name: Do snapshot ingress-nginx and create PR if snapshot changed
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Chart Snapshot for ingress-nginx
        uses: jlandowner/helm-chartsnap-action@v1
        with:
          repo:   https://kubernetes.github.io/ingress-nginx # Helmリポジトリ
          chart:  ingress-nginx # チャート名
          values: charts/remote/ingress-nginx.values.yaml # Valuesファイルパス
          update_snapshot: true # スナップショットの差分があった場合にプルリクエストを作成する

このアクションを定期的に実行することで、以下を実現できます。

チャートの新バージョンがリリースされたことが検知できる。

スナップショットには実行したチャートやアプリケーションのバージョンが含まれるため、dependabotのように、チャートが更新されるとバージョン番号の差分が出ます。

以下のようなプルリクエストが作成されます。

Screenshot

GitHub上で差分を可視化できる。

GitHub上でプルリクエストとして差分を確認できます。
Screenshot

差分が問題ない時はプルリクエストをマージするだけです!

Helmプラグインについて

今回Helmのプラグインを初めて作りました。Helmプラグインの歴史は長く、helm templateコマンドも元々プラグインとして実装されていたようです。少しわかりにくい点もあったので簡単にまとめておきたいと思います。

Helmプラグインの仕様

Helmプラグインは何かHelmコマンドにコードを組み込む機能ではなく、プラグインとして定義した任意のコマンドを実行できる機能です。

kubectlのkrewのように中央集権的なパッケージリポジトリはなく、Helmプラグインの仕様を定義したplugin.yamlという名のファイルが配置されたGitリポジトリを指定するだけでプラグインとしてインストールが可能です。

Helmの公式ドキュメント上にRelated Projects and Documentationというページがあり、プラグインのリストがあります。Helmプラグインはプラグインリポジトリはないのでプラグインの全量がリストされているわけではなく、どういう基準でリストされているのかわかりませんでした。ので、とりあえずchartsnapnの追加をプルリクエストしてみたところすぐにマージしてくれましたので、有名かどうかに関わらず問題なければマージしてくれるのかと思いました。

plugin.yamlの仕様は下記のとおりです。

いくつかのHelmプラグインリポジトリを参考に、chartsnapは以下のようなファイルにしました。

# helm plugin config file
# Ref: https://helm.sh/docs/topics/plugins/
name: chartsnap
version: 0.0.8
usage: Snapshot testing for Helm charts
description: Snapshot testing for Helm charts
command: "$HELM_PLUGIN_DIR/bin/chartsnap"
ignoreFlags: false
hooks:
  install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
  update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"

helmのプラグインドキュメントを見ると他にもいくつかプロパティがありますが、確認した既存プラグインは概ね上記のようになってました。

インストールスクリプトも大体同じで、GitHubリリースから最新のtar.gzを取得して解凍するスクリプトを実行するようになっており、install, updateに関わらずGitHubの最新リリースを見に行くようになってました。そのためchartsnapも他のリポジトリから流用させていただきましたが、GitHubの最新リリースではなく、plugin.yaml内のversionのリリースを落とすように修正しました。

最後にコマンドラインオプションについてです。helmにはいくつかグローバルオプションがありますが、helmのプラグインのスクリプトに渡るコマンドラインオプションのうち、helmのグローバルオプションと同名のオプションはプラグイン側のコマンドラインオプションとして渡ってきません。

具体的には、例えば--debugオプションをツールに実装したとして、単体で実行する時と、Helmプラグインとして実行するときで挙動が変わります。

今回のツールは次のようにGoでcobraパッケージを使用したCLIとして作成しています。

var (
    o = &option{}
)

type option struct {
	...
	// Below properties are the same as helm global options
	// They are passed to the plugin as environment variables
	NamespaceFlag string
	DebugFlag     bool
}

Cobraにフラグとして--debug--namespaceを実装しています。これらはHelmのグローバルオプションと被っていますが、単体での実行時には必要なオプションです。

func main() {
    ...
    rootCmd.PersistentFlags().BoolVar(&o.DebugFlag, "debug", false, "debug mode")
    rootCmd.PersistentFlags().StringVar(&o.NamespaceFlag, "namespace", "default", "namespace. this flag is passed to 'helm template RELEASE_NAME CHART --values VALUES --namespace NAMESPACE' as 'NAMESPACE'")
    ...
}

これらのオプションを指定した場合、単体で実行する時と、Helmプラグインとして実行するときで挙動が変わります。

# 単体で実行する場合
go run main.go --debug # o.DebugFlagはtrueになる

# Helmプラグインとして実行する場合
helm chartsnap --debug # o.DebugFlagはfalseになる

Helmプラグインとして実行する場合、Helmのグローバルオプションは一度Helmコマンド側で消費され、プラグインには環境変数として渡ってきます。

そのため、開発やテストで単体でも実行可能とするためには、コマンドラインオプション+環境変数も見に行くように実装が必要です。

今回はフラグ用の変数名はXXXFlagとし、オプションの値を参照する場合は以下のようにXXX()関数を用意して呼び出すようにしました。

func (o *option) Debug() bool {
	helmDebug, err := strconv.ParseBool(os.Getenv("HELM_DEBUG"))
	if err == nil {
		return helmDebug
	}
	return o.DebugFlag
}

func (o *option) Namespace() string {
	helmNamespace := os.Getenv("HELM_NAMESPACE")
	if helmNamespace != "" {
		return helmNamespace
	}
	return o.NamespaceFlag
}

その他、渡ってくる環境変数の一覧は下記のとおりです。
https://helm.sh/docs/topics/plugins/#environment-variables

最後に

HelmのCIの一助となれば幸いです。
手軽に使えるので、ぜひ一度使ってみてフィードバックいただけると嬉しいです。

  1. 参考記事
    https://speakerdeck.com/daikurosawa/unit-testing-helm-chart
    https://zenn.dev/lhan/scraps/1bc3e899a587c7

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0