ブルーグリーンとカナリア、何を引き換えにするのか
一言でいうと
ブルーグリーンとカナリアは、良い悪いの問題ではなく、ロールバックの速度・トラフィック制御の精度・リソースコストの間で何を買うのかという問題です。
なぜ必要なのか
デプロイを一度にすべて入れ替えると、問題が起きたときに元へ戻す時間がそのまま障害の時間になります。そのため、デプロイ戦略はすべて、「新バージョンにどれだけ少しずつさらすか」と「問題が見えたらどれだけ速く戻せるか」を調整する仕組みです。
ブルーグリーンは、同じ規模の2つの環境を用意しておき、トラフィックを一度に切り替えます。切り替えの前にprePromotionAnalysisでスモークテストを実行してグリーンが正常かを確認し、切り替えの後はscaleDownDelaySecondsを300–600秒に設定して、旧バージョンをしばらく残しておきます。この時間が過ぎると以前のバージョンのPodが終了するため、ロールバックのときに新しくPodを作成する必要があり、時間がよけいにかかります。つまりこのディレイが、そのまま即時ロールバックの有効期間です。
カナリアは、5% → 分析 → 20% → 50% → 100%のように割合を上げながら、各ステップでメトリクスによって判定します。ここでは順序が決定的です。setWeightが先に実行されてカナリアにトラフィックが流れたあとに、analysisが始まって初めて、実際のトラフィックに基づくメトリクスが集まります。順序を逆にすると、データがまったくない状態で判定することになります。
どう動くのか
自動中止のしきい値は、実運用では次のような形です。成功率0.99以上(interval 30s、count 10、failureLimit 2)、p95のレイテンシ300ms以下、エラーログ率0.005以下。組織の標準SLOが成功率99%、P95 300ms以下なら、カナリアのしきい値もそれに合わせてこそ辻褄が合います。
最もよくある落とし穴はInconclusiveです。カナリアのトラフィックが少なすぎてクエリがNaNを返すと、判定が未決のまま残り続けます。対応は2つです。default(result[0], 1)のように既定値を与えるか、analysisの前にpauseを置いてサンプルを集めます。サンプルが0なら判断できないという事実そのものを、パイプラインが知っている必要があります。そのため、よくできた分析スクリプトは、リクエスト数が0のときに通過ではなく失敗を返します。
選択基準は表にまとまります。トラフィック制御の精度はカナリアが高く(1%単位)、ブルーグリーンにはありません。ロールバックの速度はブルーグリーンが非常に速いです。リソースコストはブルーグリーンがPodの2倍です。データスキーマの変更はブルーグリーンが比較的単純です。一般的なルールは次のとおりです。ほとんどのサービスにはカナリアを、決済や認証のように1件のエラーでもコストが大きいサービスにはブルーグリーンを使います。ローリングアップデートを使うなら、標準値はmaxSurge 1、maxUnavailable 0です。
現場での姿
戦略をどれだけうまく選んでも、DBマイグレーションが絡むとロールバックが壊れます。ローリングでもブルーグリーンでも、新旧のコードが同時に動いている区間が、数十秒から数分間、必ず存在するからです。スキーマを先に変えると旧バージョンのコードが壊れ、コードを先に変えると、新しいコードがまだ存在しないスキーマを参照して壊れます。どちらを先にしても、片方が壊れます。
解決策はExpand-Contractの3ステップです。Expandで旧構造と新構造を共存させ、Migrateでバックフィルとダブルライトを行い、Contractで旧構造を取り除きます。核心はロールバックの非対称性です。Expandのロールバックは無視すればよく、Migrateのロールバックも安全ですが、Contractのロールバックは、旧カラムをすでに削除しているので、単純なコードのロールバックでは戻れません。そこで格言が残ります。Expandは自由に、Contractは慎重に。最もよくあるミスは、1つのリリースにExpandとContractを一緒に入れることです。
元に戻せないもの
デプロイ戦略の価値は、元に戻せることから生まれます。ところが、トラフィックを戻しても一緒には戻ってこないものがあります。戦略を選ぶ前に、このリストを先に確認する必要があります。
すでに送ったもの。メール、通知、決済の承認、外部システムに送ったリクエストです。新バージョンが誤って送ってしまった場合、ロールバックはそれ以上送るのを止めるだけで、すでに出ていったものについては、取り消しの案内をもう1通送るしかありません。そこで、外に出ていく副作用のある変更は、カナリアの割合を特に小さく始め、元に戻せなくなる地点をデプロイの後ろへ遅らせられないかを先に検討します。
削除されたデータ。先ほど見たExpand-ContractのContractがこれです。カラムを削除したあとは、コードだけを戻しても、そのカラムは戻ってきません。
形式が変わったまま積み上がったもの。新バージョンがキャッシュやキューに新しい形式で書き込んだあとでロールバックすると、旧バージョンがそれを読んで失敗します。キャッシュは、形式が変わるときにキーも一緒に変えることがこの問題をまるごとなくす方法で、キューなら、旧バージョンが新しい形式を読み飛ばせる必要があります。
一方向だけの移行。データを新しいストレージへ移しながら元のデータを使わないようにすると、その時点からロールバックは、その間に積み上がったものを失うという意味になります。
そのため、デプロイ計画を立てるときは、2つを一緒に書いておきます。元に戻す手順と、元に戻せなくなる地点がいつなのかです。あとのほうを書いておかないと、障害の真っ最中に「今戻してもいいのか」を初めて判断することになります。その瞬間には、誰も落ち着いていません。
次のラボですること
このPodには、Argo Rolloutsもロードバランサーもありません。そこでコンテナを2つ、8091(blue)と8092(green)で起動し、アクティブな対象をファイル1つで表現して、切り替え・ヘルスチェック・自動ロールバックをシェルで作ります。カナリアの割合の振り分けと、自動中止の判定も、同じ方法で手になじませます。トラフィックをどこへ送るかを決めるのがファイルの1行だという事実を見ると、実際のコントローラーがしていることがずっとはっきりします。