LabHub

ブログ

Helmプラグインとテスト戦略: 拡張性と品質保証

한국어English日本語


1. プラグインシステム

1.1 プラグイン構造

# plugin.yaml
name: 'my-plugin'
version: '1.0.0'
usage: 'A custom Helm plugin'
command: '$HELM_PLUGIN_DIR/bin/my-plugin'
hooks:
  install: '$HELM_PLUGIN_DIR/scripts/install.sh'
  update: '$HELM_PLUGIN_DIR/scripts/update.sh'

1.2 プラグイン管理

# プラグインのインストール
helm plugin install https://github.com/example/helm-my-plugin
helm plugin install https://github.com/example/helm-my-plugin --version 1.0.0

# プラグイン一覧
helm plugin list

# プラグインの更新
helm plugin update my-plugin

# プラグインの削除
helm plugin uninstall my-plugin

1.3 主要プラグイン

helm-diff: アップグレード前の変更プレビュー

helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade my-release ./my-chart -f values.yaml
helm diff revision my-release 2 3

helm-secrets: シークレット管理

helm plugin install https://github.com/jkroepke/helm-secrets
helm secrets install my-release ./my-chart -f secrets.yaml

helm-unittest: チャートユニットテスト

helm plugin install https://github.com/helm-unittest/helm-unittest
helm unittest ./my-chart

2. チャートテスト

2.1 helm test

リリース後にクラスタ内で実行される組み込みテストメカニズム:

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "my-chart.fullname" . }}-test-connection
  annotations:
    "helm.sh/hook": test
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['{{ include "my-chart.fullname" . }}:{{ .Values.service.port }}']
# テスト実行
helm test my-release

# タイムアウト指定
helm test my-release --timeout 5m

# テストログ取得
helm test my-release --logs

2.2 helm-unittest

クラスタなしでローカル実行されるユニットテスト:

# tests/deployment_test.yaml
suite: test deployment
templates:
  - deployment.yaml
tests:
  - it: should create deployment with correct replicas
    set:
      replicaCount: 3
    asserts:
      - isKind:
          of: Deployment
      - equal:
          path: spec.replicas
          value: 3

  - it: should set correct image
    set:
      image:
        repository: nginx
        tag: '1.25'
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: 'nginx:1.25'

2.3 ct(chart-testing)ツール

CI/CDパイプラインでのチャート変更検出とテスト自動化:

ct list-changed --target-branch main
ct lint --target-branch main
ct lint-and-install --target-branch main

ct設定ファイル:

# ct.yaml
remote: origin
target-branch: main
chart-dirs:
  - charts
chart-repos:
  - bitnami=https://charts.bitnami.com/bitnami
helm-extra-args: --timeout 600s
validate-maintainers: false

3. リンティング

3.1 helm lint

helm lint ./my-chart
helm lint ./my-chart --strict
helm lint ./my-chart -f production-values.yaml
helm lint ./my-chart --set replicaCount=3

helm lintが検査する項目:

3.2 yamllintとkubeval/kubeconform

# YAML構文チェック
helm template my-release ./my-chart | yamllint -

# Kubernetesスキーマ検証(kubeconform)
helm template my-release ./my-chart | kubeconform \
  -strict \
  -kubernetes-version 1.29.0 \
  -summary

4. スキーマ検証

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["replicaCount", "image"],
  "properties": {
    "replicaCount": { "type": "integer", "minimum": 1, "maximum": 100 },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string", "default": "latest" },
        "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
      }
    }
  }
}

スキーマはhelm installhelm upgradehelm linthelm templateの実行時に自動検証されます。


5. OCIレジストリ

helm package ./my-chart
helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts
helm install my-release oci://ghcr.io/myorg/charts/my-chart --version 1.0.0

CI/CDでのOCI活用:

# GitHub Actionsの例
# .github/workflows/helm-publish.yaml
name: Publish Helm Chart
on:
  push:
    tags: ['v*']
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Login to GHCR
        run: echo "$GITHUB_TOKEN" | helm registry login ghcr.io -u $ --password-stdin
      - name: Package and Push
        run: |
          helm package ./charts/my-app
          helm push my-app-*.tgz oci://ghcr.io/$GITHUB_REPOSITORY_OWNER/charts

6. チャートの署名と検証

helm package --sign --key 'my-key' --keyring ~/.gnupg/secring.gpg ./my-chart
helm verify my-chart-1.0.0.tgz
helm install my-release my-chart-1.0.0.tgz --verify

7. plugin.yamlの契約: 必須フィールドと非推奨フィールド

1.1の例は、現在のドキュメントが推奨する形ではありません。ドキュメントによればplugin.yamlで必須なのはnameversionの二つだけで、versionはSemVer 2に従う必要があります。残りはすべて任意フィールドです。usageはhelmが一行で出力する使い方の文字列、descriptionhelm helpに現れる長い説明です。ignoreFlagsは真偽値で、有効にするとHelmはユーザーが入力したフラグをプラグインに渡しません。引数を自前でパースするプラグインなら、この値を有効にしてHelmのフラグ解釈と衝突する余地をなくせます。downloadersはカスタムプロトコルに対応するダウンローダー機能を設定するフィールドで、Helmが知らないスキームの先にチャートが置かれている場合に使います。

本当に重要なのは、上の例で使われているcommandhooksがドキュメント上で非推奨(deprecated)と明示されている点です。それぞれplatformCommandplatformHooksに置き換えるよう案内されています。違いは名前の変更ではなく構造です。commandは単一の文字列なので、対応プラットフォームが複数あるとその文字列の中でunameによる分岐を書くか、ラッパーシェルスクリプトをもう一段挟む必要がありました。その分岐はプラグイン作者が書いたシェルコードであり、新しいアーキテクチャが登場すると黙って誤ったバイナリを実行しました。platformCommandはosとarchをキーに持つリストなので、どのバイナリを実行するかを決める責任がシェルスクリプトからHelmへ移ります。platformHooksも同じ方式で、install、update、deleteのライフサイクルフックにプラットフォーム条件を付けられるようにするフィールドです。既存フィールドを使うプラグインが今日すぐ壊れるわけではありませんが、新規に作るなら最初からplatform系を使うほうが良いでしょう。

一つ正直に断っておくことがあります。helm.shのプラグイン文書ページには、この文書がまだHelm 4向けに更新されていないというバナーが付いており、確認時点のサイトバージョンは4.2.4でした。つまり上のフィールド一覧は実際に読める最新の文書の内容ではあるものの、Helm 4基準で検証されたものではありません。特にplatformHooksの正確なYAML構造は、文書がフィールド名と用途だけを示して例を出していないため、ここで勝手に作り出すことはしません。正確なフィールドは使用中のバージョンのドキュメントで確認してください。

# plugin.yaml — ドキュメントが推奨する現在の形
name: 'my-plugin'
version: '1.0.0'
usage: 'my-plugin [flags] CHART'
description: 'Renders a chart the way our CI renders it'
ignoreFlags: false
platformCommand:
  - os: linux
    arch: amd64
    command: '$HELM_PLUGIN_DIR/bin/my-plugin-linux-amd64'
  - os: darwin
    arch: arm64
    command: '$HELM_PLUGIN_DIR/bin/my-plugin-darwin-arm64'

8. プラグインに保証される環境変数

プラグインは独立した実行ファイルですが、真空の中で動くわけではありません。Helmがプロセスを起動する際に渡す環境変数の上で動作し、ドキュメントは次の変数を保証しています。

HELM_PLUGINS            # プラグインディレクトリ
HELM_PLUGIN_NAME        # helmが呼び出した名前
HELM_PLUGIN_DIR         # このプラグインのディレクトリ
HELM_BIN                # helm実行ファイルのパス
HELM_DEBUG
HELM_NAMESPACE
HELM_KUBECONTEXT
HELM_REGISTRY_CONFIG
HELM_REPOSITORY_CACHE
HELM_REPOSITORY_CONFIG

この一覧の中で実務上決定的なのはHELM_BINとHELM_KUBECONTEXTです。プラグインがクラスタに何かを問い合わせる必要があるとき、選択肢は二つあります。kubeconfigを自分で読んでクライアントを作るか、HELM_BINが指すhelmを呼び戻すかです。後者を選ぶべきです。ユーザーが特定のコンテキストを指定してhelmを呼んだなら、Helmはその判断をHELM_KUBECONTEXTに入れて渡してきます。プラグインがkubeconfigを自前でパースするとその値を無視してcurrent-contextを読むため、ユーザーはステージングを指したのにプラグインだけが本番を見る状態になります。同じ理由でネームスペースもHELM_NAMESPACEに従うべきで、リポジトリキャッシュを見る必要があればHELM_REPOSITORY_CACHEとHELM_REPOSITORY_CONFIGを、OCIレジストリの認証情報が必要ならHELM_REGISTRY_CONFIGを使うのが正しい形です。これらのパスをハードコードすると、複数のhelm設定を行き来するCIランナーで即座に壊れます。

#!/usr/bin/env bash
set -euo pipefail

# 診断用: helmが実際に渡してきた値をまず出力する
echo "plugin=$HELM_PLUGIN_NAME dir=$HELM_PLUGIN_DIR" >&2
echo "namespace=$HELM_NAMESPACE context=$HELM_KUBECONTEXT" >&2

# kubeconfigを直接読まず、呼び出し元が渡したhelmへ呼び戻す
exec "$HELM_BIN" template "$@"

HELM_DEBUGも役に立ちます。ユーザーがhelmをデバッグモードで実行したという信号なので、プラグインのログ詳細度をこの値に合わせれば別途フラグを作る必要がありません。逆にプラグインが独自のデバッグフラグを持ちHELM_DEBUGを無視すると、ユーザーはhelm側のデバッグを有効にしたのに、肝心の失敗しているプラグインだけが沈黙するという状況になります。

9. 一つのチャートをゲートに順番に通す

ここまでの道具はそれぞれ単体で紹介すると良さそうに見えますが、実際には順序が意味を作ります。後の段階ほど遅く高くつくので、前で弾けるものを後ろに回すとフィードバックループが長くなるだけです。一つのチャートを次の順で通すのが基本形です。

# 1) 静的検査 — クラスタなし
helm lint ./my-chart --strict --with-subcharts

# 2) レンダリング結果をKubernetesスキーマで検証 — クラスタなし
helm template my-release ./my-chart \
  | kubeconform -strict -kubernetes-version 1.29.0 -summary

# 3) 値の分岐ごとの単体検証 — クラスタなし
helm unittest ./my-chart

# 4) シミュレーション
helm install my-release ./my-chart --dry-run

# 5) 実際にデプロイした後、クラスタ内で検証
helm install my-release ./my-chart --wait
helm test my-release --logs

各段階が捕まえるものは異なります。helm lintはチャートがwell-formedか、つまりChart.yamlの必須フィールドがあるか、テンプレートがそもそもレンダリングされるかを見ます。--strictは警告を失敗に昇格させ、--with-subchartsは依存チャートまでリントします。ここを通ったということは「文字列が生成された」という意味でしかなく、その文字列がKubernetesの受け入れるマニフェストである保証はありません。その隙間を埋めるのが2段階目です。kubeconformはレンダリング済みYAMLを指定したKubernetesバージョンのOpenAPIスキーマに照らすので、spec.replicaのような綴り間違いやapiVersionが失われたリソースをここで捕まえます。lintはこれらを決して見つけられません。

3段階目は、前の二つが構造的にできないことをします。lintとkubeconformはデフォルト値一組でレンダリングした結果しか見ませんが、実際の事故はたいてい特定のvalues組み合わせで起きます。helm unittestは値を変えながらレンダリングして結果の特定パスを断言するので、ingressを無効にするとドキュメントが0個になる、replicaCountを3にすると実際に3になる、といった分岐ごとの契約を固定します。ただしこれも依然としてローカルのレンダリングです。

4段階目で初めてシミュレーションが登場します。helm install --dry-runはドキュメント上none(デフォルト)、clientserverのいずれかを取ります。lintが--kube-versionで人間が教えたバージョンを基準にcapabilitiesとdeprecationを判断するのと対照的な地点ですが、それぞれの値がどこまでAPIサーバーを往復するかはバージョンによって変わってきました。正確なフィールドは使用中のバージョンのドキュメントで確認してください。実務的に覚えることは一つです。ここまで通っても、イメージが実際に取得できるか、Serviceが応答するかは誰も確認していません。

5段階目がその場所です。helm testはリリースがインストールされたクラスタの中でテストフックをPodとして起動し、そのPodが成功すればリリースが実際に動作するとみなす方式です。成功出力には次のようなブロックが付き、ドキュメントが示す判定行はPhase: Succeededです。

NAME: demo
LAST DEPLOYED: Mon Feb 14 20:03:16 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE:     demo-test-connection
Last Started:   Mon Feb 14 20:35:19 2022
Last Completed: Mon Feb 14 20:35:23 2022
Phase:          Succeeded

フックのアノテーションは値が三種類ありますが、地位が異なります。現在の標準は"helm.sh/hook": testの一つです。test-successはHelm v3まで使われていた値で、今も後方互換のために受け付けられ、test-failureは非推奨です。古いチャートを引き継いだならこれらが混在している可能性があるので、testに統一するのが安全です。ここに通常のフックで使うhelm.sh/hook-weighthelm.sh/hook-delete-policyもそのまま適用されます。hook-weightは文字列として書く必要のある数値で、同じKindの中で昇順に整列されるため、データを投入する準備Podを先に、検証Podを後に回すといった順序を作れます。hook-delete-policyはbefore-hook-creation(デフォルト)、hook-succeededhook-failedから選びます。

# templates/tests/test-connection.yaml — アノテーション部分
metadata:
  annotations:
    'helm.sh/hook': test
    'helm.sh/hook-weight': '10'
    'helm.sh/hook-delete-policy': hook-succeeded

10. values.schema.jsonを運用で使う

スキーマファイルはドキュメントの代わりではなくゲートです。ドキュメントは検証がhelm installhelm upgradehelm linthelm templateの四つの命令で起きると明記しています。この一覧で重要なのは、lintとtemplateが含まれている点です。おかげで誤ったvaluesはクラスタに届く前に、それどころかクラスタのないCIランナー上でも捕まります。9節の1段階目がスキーマゲートを兼ねるという意味です。

二つ目に知っておくべき規則はサブチャートに絡みます。ドキュメントは最終的な.Valuesオブジェクトがすべてのサブチャートのスキーマに対して検査されると述べています。親チャートはサブチャートが課した制約を回避できず、その制約を自らも満たさなければなりません。実務ではたいていこう現れます。親チャートのvalues.yamlがサブチャートのキーを上書きし、サブチャートのスキーマがそのキーにenumやminimumを掛けていると、親だけを見ていた人には突然現れた検証失敗に見えます。エラーメッセージが指すパスが親のスキーマにないなら、次に開くべきはサブチャートのスキーマです。

三つ目は脱出口です。スキーマがリモート参照を含むと、閉域網では検証そのものが失敗します。そのために--skip-schema-validationがあります。helm installhelm lintの両方がこのフラグを文書化しており、説明はいずれもJSONスキーマ検証を無効にするというものです。ただしこれは閉域網の回避策であって、検証が煩わしいときに使うスイッチではありません。チームのCIでこのフラグが常時付いているなら、そのスキーマはすでに死んだファイルです。

# スキーマだけ確認したいとき: レンダリングまで行かずlintで終える
helm lint ./my-chart -f production-values.yaml

# 閉域網でリモート参照を含むスキーマに阻まれるときだけ
helm install my-release ./my-chart --skip-schema-validation

11. 失敗事例と診断の順序

lintは通るのにinstallが失敗するケースが最も多く見られます。これはバグではなく設計です。lintはAPIサーバーに問い合わせず、クラスタのバージョンすら--kube-versionで人間が教えます。したがってRBAC不足、既に存在する名前、admission webhookによる拒否、存在しないCRDといったものはlintの視界の外です。診断順序はレンダリング結果を目で読み、kubeconformでスキーマを確認し、その後--dry-runで上げてみることです。前の二段階で何も出ないなら、問題はチャートではなくクラスタ側にあります。

ユニットテストのスナップショットが揺れるケースもよく見ます。症状はコードを触っていないのにCIだけ赤いことです。原因はたいてい、スナップショットがレンダリング結果全体を丸ごと掴んでいて、その中にチャートバージョンやイメージタグのようなリリースごとに変わる値が混ざっていることです。-u--update-snapshot)で更新すれば赤は消えますが、それは診断ではなく沈黙です。まずdiffを読み、変わった行が意図した変更かを確認し、変動値が原因ならその部分はスナップショットではなくパス単位の断言に置き換えるべきです。

プラグインがある日突然誤ったクラスタに触れる事故は、8節で述べた原因そのものです。症状は特徴的で、helm本体は正しいコンテキストで動作するのにプラグインだけが別のクラスタの結果を返します。確認順序は、プラグインのスクリプトでkubeconfigやKUBECONFIGを直接読んでいる行を探し、その箇所をHELM_BIN経由の呼び戻しに変えることです。

スキーマ検証がサブチャートのバージョンを上げた後にだけ失敗するケースは、10節の規則がそのまま現れたものです。親チャートはそのままなのに失敗するので原因が見えません。サブチャートの新バージョンがvalues.schema.jsonを追加したか、既存スキーマに制約を足したかをまず確認してください。

最後にhelm testがPodを残す問題があります。テストPodにhook-delete-policyがないとデフォルトのbefore-hook-creationが適用され、次の実行の直前まで生き残ります。ネームスペースにCompleted状態のPodが溜まるのが正常な動作だという意味です。ログを残す必要がある状況でなければhook-succeededを付けるほうが良いでしょう。

# 診断順序をそのまま
helm template my-release ./my-chart | less        # まずレンダリング結果を目で
helm template my-release ./my-chart | kubeconform -strict -summary
helm install my-release ./my-chart --dry-run      # ここで初めてシミュレーション
kubectl get pods -l 'app.kubernetes.io/instance=my-release'

12. 使わないほうがよい場合

ユニットテストがテンプレートをそのまま読み上げる形なら、書かないほうが良いでしょう。spec.replicas.Values.replicaCountと同じかを断言するテストはテンプレートをもう一度書き写したものなので、テンプレートが変われば必ず一緒に変わります。そうしたテストは回帰を捕まえず、変更コストだけを二倍にします。価値のある断言は分岐です。特定の値の組み合わせでリソースが出るのか出ないのか、条件ブロックが有効になるのか、といったものです。

プラグインも同様です。valuesファイル一つやMakefileの数行で片付く仕事をプラグインにすると、配布、バージョン管理、プラットフォーム別バイナリという負担が新たに生まれるだけです。プラグインが値打ちを持つのは、helmのサブコマンドのように見えつつhelmのコンテキストとネームスペースをそのまま引き継ぐ必要があるときです。その要求がないならシェルスクリプトのほうが優れています。

helm testを実際のスモークテストの代替として使うことにも注意が必要です。テストフックはリリースがインストールされた直後にクラスタ内部で動くPodなので、イングレスの外側でのTLS、外部DNS、認証ゲートウェイ、実際のトラフィック経路はすべて確認範囲の外です。デプロイパイプラインに別途のエンドツーエンド検証があるなら、helm testはその手前の安価な確認として置き、それで十分だと宣言はしないでください。

13. 参考資料

14. まとめ

Helmの拡張性と品質保証:

  1. プラグインシステム: helm-diff、helm-secrets、helm-unittestで機能拡張
  2. 多層テスト: helm test(統合)、unittest(ユニット)、ct(CI/CD)の組み合わせ
  3. リンティング: helm lint、yamllint、kubeconformによる多角的検証
  4. スキーマ検証: values.schema.jsonで入力値の妥当性を保証
  5. OCIレジストリ: コンテナイメージと同じワークフローでチャートをデプロイ
  6. 署名/検証: チャートの整合性と出所の証明

コメント

まだコメントはありません。

ログインするとコメントできます