リリース — クラスタの中に残るデプロイの記憶
一言でいうと
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を直接確認したあと、ライフサイクルのレポートを作ります。