TT Lab
はじめる
学ぶ 学習パス コース

Helmチャートの作成とデプロイ

リリース — クラスタの中に残るデプロイの記憶

TT Labで続きを見る

一言でいうと

releaseはコマンドの記録ではなく、クラスターの中に保存された状態で、その状態はリビジョンごとにSecret1つとして積み重なります。

なぜ必要なのか

kubectl applyでデプロイすると、「今何が動いているか」はわかっても、「昨日は何が動いていたか」はわかりません。問題が起きたときに元に戻すには、以前のマニフェストをどこかから探してくる必要がありますが、そのどこかが、人の記憶か、誰かのノートPCであることがほとんどです。

Helmは、この問題を「デプロイのたびに、そのデプロイの完全なスナップショットをクラスターに残す」ことで解決します。残るのは、レンダリングされたマニフェストだけではありません。そのとき使ったチャートのメタデータ、ユーザーが渡した値、状態、releaseのノートが、まるごと入ります。そのため、ロールバックが「昔のファイルを探すこと」ではなく、「保存されたリビジョンを選ぶこと」になります。

どう動くのか

Helm 3には、クラスターの中に常駐するサーバーコンポーネントがありません。CLIがkubeconfigでAPIサーバーと直接やり取りするので、KubernetesのRBACがそのまま適用されます。では、状態はどこに置くのでしょうか。releaseがインストールされたネームスペースのSecretに置きます。

項目 値
Secretの名前 sh.helm.release.v1.<릴리스이름>.v<리비전번호>
Secretのタイプ helm.sh/release.v1
ラベル owner=helm、name=<릴리스이름>、status=<상태>
内容 チャートのメタデータ、レンダリングされたマニフェスト、values、状態、ノートをgzip圧縮したあとbase64エンコード

表の山括弧の中の韓国語はプレースホルダーで、順にrelease名、リビジョン番号、release名、状態です。

リビジョンごとにSecretが1つずつ増えます。そのため、kubectl get secret -l owner=helmを見るだけで、そのネームスペースのデプロイの履歴が何層あるかがわかります。デフォルトでは、最近の10個まで保持し、--history-maxで調整します。

アップグレードは、単純な上書きではありません。Helm 3は、3つを比較します。前のリビジョンのマニフェスト、今のクラスターの実際の状態、そして新しくレンダリングしたマニフェストです。これがthree-way strategic merge patchです。そのおかげで、誰かがkubectlで直接触ったフィールドに気づき、チャートが管理しないフィールドには触れず、変わったものだけを選んで適用します。

そして、必ず覚えておくべき2つの性質があります。

1つ目は、失敗したアップグレードもリビジョンとして残ることです。レンダリングはできたのに、APIサーバーが拒否した場合、そのリビジョンはfailed状態として記録され、実体は前の状態のまま残ります。履歴に失敗が残るのは事故ではなく機能です。何を試みて、なぜだめだったのかが、クラスターに残ります。

2つ目は、ロールバックは元に戻すことではなく、新しいリビジョンを作ることだということです。リビジョン2にロールバックすると、番号が2に戻るのではなく、リビジョン2のチャートとvaluesで計算した新しいリビジョン4ができます。リビジョン番号は、常に前にだけ進みます。この性質のおかげで、「ロールバックしてから、そのロールバックを取り消す」ことも、履歴にそのまま残ります。

値を調べる方法も2通りです。helm get valuesはユーザーが実際に渡した値だけを、-a(--all)を付けると、チャートのデフォルト値までマージされた最終的な値を見せます。--revision Nを一緒に渡せば、特定のリビジョンの値を見られます。障害対応で「この設定はデフォルト値か、誰かが入れた値か」を切り分けるのが、この2つのコマンドの違いです。

現場での姿

1つ目は、止まってしまったpending状態です。アップグレード中にプロセスが落ちると、releaseがpending-upgradeのまま残って、次のコマンドが拒否されます。このとき必要なのは、Secretを手で消すことではなく、最後に成功したリビジョンにロールバックして状態を整理することです。

2つ目は、--atomicの2つの顔です。--atomicは--waitを含み、失敗時に自動で元に戻してくれるので、パイプラインに向いています。ところが、自動で元に戻ってしまうと、失敗した状態を観察する機会がなくなります。原因を見る必要がある状況では、あえて付けません。

3つ目は、値のドリフトです。誰かが--setで急いでレプリカ数を上げて障害を乗り切ったなら、その値はそのリビジョンにしか存在しません。次のデプロイが、それを静かに元に戻します。そのため、一時的に入れた値は、必ずリポジトリの値ファイルに反映する必要があります。

releaseが詰まったときに解く順序

Helmは、デプロイがうまくいくときは楽ですが、一度ずれると、状態を直接直す必要があります。よく見るのは3つです。

pending-upgradeに閉じ込められます。アップグレードの途中でCIが落ちたりタイムアウトしたりすると、releaseがその状態で残り、次のhelm upgradeが「another operation is in progress」で拒否されます。実際に動いている作業がないことを確認してから、元に戻します。

helm history myapp -n prod
helm rollback myapp <마지막 deployed 리비전> -n prod

--atomicと--waitは別のことをします。--waitはリソースの準備ができるまで待ち、--atomicは待っていて失敗したら自動で元に戻します。パイプラインでは、--atomic --timeout 10mを一緒に使うほうがよいです。そうしないと、半分だけ適用された状態が残ります。

フックがデッドロックを作ります。pre-upgradeフックでマイグレーションのJobを実行するのに、そのJobが新しいバージョンのイメージを使い、そのイメージはアップグレードが終わらないとデプロイされない構造なら、永遠に待ちます。フックにはすでにあるイメージを使い、hook-delete-policyで、失敗したJobを残して、ログを見られるようにします。

annotations:
  "helm.sh/hook": pre-upgrade
  "helm.sh/hook-weight": "-5"
  "helm.sh/hook-delete-policy": before-hook-creation

値がどこから来たかを確認します。-fが複数あって--setが混ざると、最終的な値がわからなくなります。あとに来るものが勝ち、--setはファイルより強いです。

helm get values myapp -n prod --all      # 실제로 쓰인 값(기본값 포함)
helm template . -f values-prod.yaml | kubectl diff -f -

CRDはアップグレードで更新されません。crds/ディレクトリのものは、インストールのときだけ適用されます。チャートを上げたのに新しいフィールドが効かなければ、ここです。CRDは別に適用する必要があります。

次のラボですること

ネームスペースhelm-labにreleaselab-appをインストールしてリビジョン1を作り、レプリカ数を上げてリビジョン2を作ります。そのあと、APIサーバーが拒否する値をわざと入れて、失敗したリビジョン3を作り、リビジョン2にロールバックして、リビジョン4を得ます。helm get valuesを-a付きと、なしで、それぞれ抜き出して比べ、ネームスペースに積み上がったreleaseのSecretを直接確認したあと、ライフサイクルのレポートを作ります。