フックはリリースの外に住んでいる
一言でいうと
フックのリソースは、releaseが管理しません。そのため、ロールバックしても消えず、削除ポリシーを掛けないと、クラスターに積み上がります。
なぜ必要なのか
DBマイグレーションをデプロイに組み込む最もよくある方法が、pre-upgradeフックのJobです。ところが、このJobはreleaseの一部ではありません。Helmは、フックを適用して、完了を待ち、そのあとで、本体のマニフェストを適用します。フックで作られたJobオブジェクトは、releaseのSecretに入りません。
結果は2つです。
- ロールバックしても、フックがやったことは残ります。マイグレーションが変えたスキーマは、そのままです。
- Jobオブジェクトが積み上がり続けます。
helm.sh/hook-delete-policyを掛けないと、デプロイのたびに1つずつ増えます。
どう動くのか
metadata:
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5" # 작을수록 먼저
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
hook-delete-policyの値3つは、意味が違います。
| 値 | いつ削除するか |
|---|---|
before-hook-creation |
次のデプロイで、同じフックを作る直前(失敗したJobを残して、調査できる) |
hook-succeeded |
成功したら、すぐに |
hook-failed |
失敗したら、すぐに(ログが消える。たいてい悪い選択です) |
実務の基本形は、before-hook-creation,hook-succeededです。成功したものは片づけ、失敗したものは次のデプロイまで残して、ログを見られるようにします。
よくある勘違い
フックが失敗するとロールバックされるという勘違いです。pre-upgradeフックが失敗すると、アップグレードが中断され、releaseはfailedのまま残ります。--atomicがあれば、ロールバックが掛かりますが、すでに実行されたマイグレーションは元に戻りません。
フックでやってはいけないこと
フックは便利なので、あらゆるものを入れたくなります。入れてはいけないものが3つあります。
時間のかかるデータ移行です。数百万行を移す作業は、デプロイを数十分間拘束します。その間、releaseがpending-upgradeでロックされて、ほかのデプロイが止まります。こうしたものは、デプロイと分離して別のJobで実行し、アプリケーションが2つのスキーマを両方読めるようにします。
外部システムに知らせる作業です。post-installフックでSlackに知らせるのはよくありますが、フックが失敗すると、デプロイ全体が失敗と表示されます。通知が届かなかったからといって、デプロイを失敗と見る理由はありません。こうしたものは、CI側に置きます。
テストです。helm testは別のコマンドで、helm.sh/hook: testを使います。デプロイの過程に混ぜると、テストが不安定になるたびに、デプロイが止まります。
フックが引っかかるとデプロイが止まる
フックのJobが終わらなければ、helm upgradeはずっと待ちます。--timeoutのデフォルトは5分で、その後は失敗として扱います。ところが、タイムアウトになっても、Jobはクラスターで動き続けます。Helmは、待つのをやめるだけで、Jobを殺しません。
ここで危険な状況が生じます。失敗と判断してもう一度デプロイすると、前のマイグレーションがまだ動いているのに、2つ目が始まります。同じテーブルにALTERを2つ同時に掛けると、ロック待ちでサービス全体が止まることがあります。
3つの方法で防ぎます。
- Jobに
activeDeadlineSecondsを掛けます。Helmのタイムアウトより短くしておけば、Jobが自分で先に終わります。 backoffLimit: 0です。マイグレーションは、リトライが安全でない場合が多いからです。- マイグレーションツールのロックを使います。Flyway・Liquibase・Alembicは、どれもロックテーブルがあり、2つ目の実行が待ちます。自作のスクリプトなら、
pg_advisory_lockを使います。
spec:
activeDeadlineSeconds: 240 # helm --timeout 5m 보다 짧게
backoffLimit: 0
template:
spec:
restartPolicy: Never
フックと通常のマニフェストの順序
1つのチャートの中にフックと通常のリソースが混ざっていると、順序が混乱します。実際の順序は、次のとおりです。
pre-install 훅 → 일반 매니페스트 적용 → post-install 훅
(weight 순) (Helm 의 종류별 순서) (weight 순)
ここで、よく踏む落とし穴があります。pre-installフックは、SecretやConfigMapがまだない状態で動きます。フックのJobがreleaseのSecretを参照すると、「そんなsecretはない」で失敗します。フックが使う値は、フック自身が作る(同じフックグループに、weightを低くしたSecret)か、releaseの外で事前に作っておく必要があります。
hook-weightは、文字列ですが、数値でソートされます。"10"と"9"を渡すと、9が先です。負の数を使う慣例が、ここから出てきました。-5を基本として、さらに先に動く必要があるものに-10を与えます。
実務で本当に大切なこと
元に戻せないマイグレーションは、デプロイと分離します。カラムを削除する変更は、3回に分けてデプロイします。まず新しいカラムを追加し(古いコードも動作)、次に新しいコードをデプロイし、最後に古いカラムを削除します。こうすれば、どの段階でロールバックしても、前後のバージョンがどちらも耐えられます。これを拡張-縮小(expand-contract)パターンと呼びます。