AWSにおけるALB&NLBのBlue/Greenデプロイメント設計
はじめに
どうも、iselegant です。 前回、執筆した商業誌について本ブログで紹介させていただいたところ、大変多くの反響がありました。 コメントをくれた方、書籍に関心を持っていただいた方、本当にありがとうございます🙇
AWSコンテナ設計・構築[本格]入門 | 株式会社野村総合研究所, 新井雅也, 馬勝淳史, NRIネットコム株式会社, 佐々木拓郎 |本 | 通販 | Amazon
本日から少しの間、分量調整と締め切りの都合上、商業誌では執筆しきれなかった AWS 設計に関するサイドトピックについて、本ブログ上でご紹介したいと思います。
今日はALB (Application Load Balancer) と NLB (Network Load Balancer) の Blue/Green デプロイメントに関する設計がテーマです。 AWS で Web アプリケーションの可用性とパフォーマンスを高める上で、ALB と NLB は主要かつ重要なサービスです。 本ブログを読んでいる皆さまも、ALB・NLB いずれかは利用されたことがあるのではないでしょうか?
実は、 NLB でデプロイメント戦略を実現する場合、事前に押さえておくべき内部仕様とそれに基づく設計ポイントがあります。
僕はこの仕様を知らなかったがために、実務で大変苦労しました。 またNLBのドキュメント上に正確な記載が見当たらず、他のエンジニアの方が NLB を利用する際に同じ思いをしないように・・・
内容がかなり複雑、かつ長い文章になってしまいましたが、、、AWS でロードバランサーの仕組みに興味のある方はぜひお付き合いくださいmm
TL;DR
- ALB と NLB で Blue/Green デプロイメント時の振る舞いが異なる
- ALB は Replacement 側タスクが応答するまで 10-20 秒だが、NLB の場合は 60秒以上の時間がかかる
- NLB は適切に設計しないと、デプロイメント時およびロールバック時に通信エラーとなる可能性がある
- NLB ではダミーリスナーを設けることで、通信エラーを回避できる
Blue/Green デプロイメントのおさらい
まず簡単に Blue/Green デプロイメントに関しておさらいします。
Blue/Green デプロイメントは、システムのダウンタイムを最小化しながら切り替え・切り戻しが可能なリリースの運用方式です。 比較的に安全なリリースが求められるアプリケーションでは、この Blue/Green デプロイメントが採用されるケースも多いはずです。 Amazon ECS で Blue/Green デプロイメントを構築する場合、AWS CodeDeploy + ALB or NLB と連携します。
正常なデプロイメントの流れ
ELB+ECSを例に流れを追っていきましょう。
次の例では、ロードバランサーのリスナーとしてプロダクション用とテスト用が用意されています。 また、ターゲットグループとして Blue と Green があり、初期状態では Blue 側ターゲットグループが Original (切り替え前)タスクに紐付いています。
Blue/Green デプロイメントが開始されると、次のように、まずリリース対象のアプリケーションである Replacement (切り替え先)タスクが起動します。
そして、テスト用リスナーと Green 側ターゲットグループの関連付けや、Green 側ターゲットグループから Replacement 側タスクに対してヘルスチェックを実施します。 ヘルスチェックに合格すると、テスト用リスナーにアクセス可能なクライアント(管理者)のみが、Replacement側ECSタスク側に対して事前にリクエストできます。 この段階では、プロダクション用リスナーを介してアクセスするユーザからReplacement 側タスクにはアクセスできません。 ※後述しますが、切替前に事前の猶予時間を設けるかどうかは、AWS 利用者側の設定次第です。
テスト用リスナーを介して Replacement 側タスクのアプリケーションが正常に動作していると判断できた後、プロダクション用リスナーのトラフィックを Green 側ターゲットグループに切り替えます。 こうすることで、ユーザは Replacement 側タスクに対して、アクセスが可能となります。
旧から新にアプリケーションが切り替わった後、Original 側タスクが停止することで Blue/Green デプロイメントの一連の処理が完了となります。
以上がBlue/Greenデプロイメントの簡単な流れでした。
最初の状態として、どちらのリスナーも Blue 側ターゲットグループに寄っている状態から、Blue/Green デプロイメントが完了した後は Green 側ターゲットグループに寄った状態となっていますよね。 次にまた別のリリースを行う場合、 Green → Blue 側に寄るような動きになります。
ロールバックによる切り戻し
さて、ここで Step.4' の通り、ユーザに Replacement 側タスクに切り替えた状態から、アプリケーションの不具合を検知してロールバックしたいケースを考えましょう。
ロールバックを実行すると、Green 側ターゲットグループに片寄せされていたトラフィックが、一斉に Blue 側ターゲットグループに切り替わります。 この切り替わりにより、 Step.5' のように切り替え前の Original 側タスクに対してトラフィックが流れることになり、アプリケーションとしても旧の状態に戻ります。
そしてロールバックが完了すると、Step.6' のように、もともとリリース予定だった Replacement 側タスクが破棄されます。
以上がロールバック時の流れです。
通常のデプロイメントフローの場合も、ロールバックにおいても、ロードバランサー内部のリスナーとターゲットグループの転送先がバチッと切り替わります。 そのため、即時かつ安全な切り替え・切り戻しが実現できていそうに"見えます"よね。 この「見えます」というのが、今日のお話のポイントであり、NLB 利用時における注意点となります。
ALB と NLB の Blue/Green デプロイメント
ここからが今日の本題です。 Blue/Green デプロイメントを利用することの最大のメリットはなんでしたでしょうか?
Blue/Green デプロイメントは、システムのダウンタイムを最小化しながら切り替え・切り戻しが可能なリリースの運用方式です。
そうです、「ダウンタイムの最小化」です。 ALB と NLB 両方で Blue/Green デプロイメントを実装された方はご存知かもしれませんが、実は ALB と NLB でこのダウンタイムの側面が大きく異なります。 どういうことでしょうか? 横軸を経過時間として、アプリケーションに対する疎通状況、リスナー、ターゲットの3つの状態遷移を図で見ていきましょう。
※情報の正確性の観点から検証時の生ログ等を適宜参照しながらお伝えすべきなのですが、内容の複雑さ故に少しデフォルメした図で要点をお伝えします。
ALB における Blue/Green デプロイメント
まずは ALB のパターンです。 吹き出しの「 ECS サービス更新 ( Blue/Green デプロイメント実行) 」が先程の図の Step.1 → Step.2 、「タスクセットの終了( Original 側 ECS タスクの停止)」が Step.4 → Step.5 への遷移に該当し、それぞれCodeDeploy 側の操作になります。
今回は簡略化のために、事前の猶予時間設定によるテストリスナー事前確認 ( Step.3 ) は省略しています。
順番にポイントを見ていきましょう。 Blue/Greenデプロイメント開始から約 1 分後に Green 側ターゲットの状態として unused になります。 このunused状態ですが、Replacement 側タスクが起動しているものの、まだ Green 側ターゲットグループがリスナーから利用されていない状態を表しています(つまり、先程の Step.2 の状態です)。
そして、開始から約 2 分 20 秒後にプロダクション用リスナーとテスト用リスナーが Green 側ターゲットグループに向きます。 テストリスナーによる事前確認 (Step.3) を省略したため、プロダクション及びテスト用リスナーが同時に切替わる動きとなります。
Blue 側ターゲットグループはいずれのリスナーにも紐付かなくなるため、ターゲットの状態として unused 判定となります。 一方、Green 側ターゲットグループが initial 状態にも関わらず、リスナーの向き先が Green 側のターゲットグループに切り替わると、10-20 秒後に Replacement 側タスク上のアプリケーションが応答するようになります。
Green 側ターゲットグループの initial 状態を追ってみると、「Elb.RegistrationInProgress」となっており、リスナーとターゲットグループの関連付けの処理中となっているようでしたが、実際には紐付けが完了したあともしばらくinitial 状態が表示されているものと推察されます。
以上が ALB における Blue/Green デプロイメントの動きです。
まとめると、プロダクションリスナー側が Green 側ターゲットグループに関連付けされてから、10-20 秒後に Replacement 側タスク上のアプリケーションが応答するという結果になります。
NLB における Blue/Green デプロイメント
次にNLBのパターンをみてみましょう。
先程の ALB 同様、簡略化のためにテストリスナーによる事前確認 ( Step.3 )は省略しています。
順番にポイントを見ていきます。 Blue/Green デプロイメント開始から約 60秒後にGreen 側ターゲットの状態として unused になります。 この点は ALB と同じです。
開始から約 2 分 40 秒後にプロダクション用リスナーとテスト用リスナーが Green 側ターゲットグループに向きます。 ALB のケースでは、ここから 10-20 秒後には Replacement 側タスク上のアプリケーションが応答していましたが、NLB の場合は 60 秒以上要していることがわかります。
関連してみられる特徴として、Green 側ターゲットグループの initial 状態の長さです。 先程の ALB の例では、initial 状態は Elb.RegistrationInProgress のみでしたが、NLB の場合、initial 状態が Elb.RegistrationInProgress ( 10 秒程度) + Elb.InitialHealthChecking ( 60-70 秒程度)となります。 これは、NLB の内部仕様であり、ALB と比較してターゲットの登録プロセスが完了するまで 90-180 秒要します。 この点はドキュメントに以下のように少々曖昧な形でシレッと記載されており、90-180秒という数値はAWSサポートからの回答により判明しました。
ターゲットグループのヘルスチェック - Elastic Load Balancing
ターゲットを 1 つ以上のターゲットグループに登録します。登録プロセスが完了次第、ロードバランサーは新しく登録したターゲットへのトラフィックのルーティングを開始します。登録プロセスが完了し、ヘルスチェックが開始されるまで数分かかることがあります。
少し言い換えて表現すると、リスナーから孤立したターゲットグループがリスナーと紐付けられ、ヘルスチェックが合格するまでの時間に 90-180 秒要するということになります。
また、「 Elb.InitialHealthChecking 」は最初のヘルスチェックが実行中の旨を表しますが、この状態に該当する時間は「利用者が設定したターゲットグループのヘルスチェック回数 ✕ 間隔(s)」とは一致しません。 そのため、ヘルスチェック間隔と回数を小さくしても、NLBの仕様として「 Elb.InitialHealthChecking 」は 60秒以上かかることが仕様上わかっています。
以上のような NLB の内部仕様により、リスナーの向き先が Green 側のターゲットグループに切り替わったとしても、 ALB と比較して Replacement 側タスク上のアプリケーションが応答までに70-80 秒程度要してしまうのです。
ただ、説明はこれで終わりではありません。 今まで述べてきた NLB のケースは正常にデプロイメントが完了した場合ですが、NLB ではタイミングイシューにより、アプリ応答が一定時間エラー (無応答) となってしまうケースもあります。 ちょっと何言っているのかわからないと思うので、次の図で見てみましょう。
この図は僕が検証した結果を反映した内容ですが、Replacement 側タスク上のアプリ応答が途絶えてしまうケースが発生します。
エラーが発生している原因として、リスナーがまだ内部的に Green 側のターゲットグループに対して紐付けが完了しておらず、その間に Blue 側のターゲットグループが認識しなくなり、通信がロストしたのではないか、と推察できています( AWS サポート側にて実際に検証いただいた結果から観察された挙動だそう)。
実測ベースですが、タイミングイシューとは言え、この通信エラーは 10 回中 2〜3 回は遭遇するぐらいの頻度でした。 いずれにしても、この状態では普通に Blue/Green デプロイメントを実行しているだけなのに、通信エラーの発生を考慮しなければなりません。 ALB と比較すると、少しだけ残念な結果となってしまいました。
NLB エラー発生に対する対処
このエラーに対するワークアラウンドとして、「リスナーが内部的に Green 側のターゲットグループに対して紐付けが完了するまで猶予時間を与える」ことで、回避できます。 CodeDeploy では、プロダクション用リスナーを Green 側ターゲットグループに紐付ける前にテスト期間を設けることができます(冒頭で紹介した Step.3 の状態を維持する時間を設定できます)
NLB の内部仕様的に 90-180 秒かかるのであれば、CodeDeploy の切り替え前猶予時間を 5 分以上に設定することで、時間ベースで回避が期待できます。 実際のところ、この値を設定しておよそ 240 秒に「再ルーティング( Original → Replacement へ切り替え)」を実行すると、先程の通信エラーは発生しなくなることが確認できています。 ※ただ、この間にテストリスナーからのアプリ疎通は一時的に途絶えるケースが起こります。
CodeDeploy でルーティング前の猶予時間を設定すれば、エラーの発生自体は回避できます。 ただ、再ルーティングからReplacement 側タスク上のアプリが応答できるまで、結局 60-90 秒近く要する点は先程と同様です。 エラーは回避できるようになるものの、NLB は最終的な置き換え反映までに時間がかかると考えておいたほうが良いでしょう。
※一方、この猶予時間の設定におけるワークアラウンドはあくまで暫定対応です。その理由は後述します。
ALB と NLB の Blue/Green デプロイメントロールバック
ここまでの内容でかなりお腹いっぱい状態ですが、NLB にはもう一つ考慮が必要な仕様があります。 それはロールバック時においても、ターゲットの再登録プロセスが 90-180 秒要する点です。 先程と同じように ALB と比較しながら具体的に説明しましょう。
ALBにおけるロールバック処理
ALB の場合、ロールバックを実行すると、両方のリスナーは即座に Green→Blue 側ターゲットグループに切り替わります。
Blue 側ターゲットグループは unused 状態から initial 状態となりますが、Blue 側のターゲットが受付可能となったタイミングでアプリケーション応答が Original 側となります。 ロールバック実行から Original 側アプリ応答まで実測ベースで 10-20 秒程度必要であり、その間通信エラー等は特に発生しません。
NLB におけるロールバック処理
では、NLB に関するロールバックはどうでしょうか?
先程説明したとおり、ロールバックを実行する Step.4' の状態から Step.5' のようにリスナーの向き先が Blue 側ターゲットグループ側に切り戻されます。
ここで、Step.4' の時点では、Blue 側ターゲットグループはいずれのリスナーにも紐付いていない状態となってしまいます。
先程と同じように、「リスナーから孤立したターゲットグループがリスナーと紐付けられ、ヘルスチェックが合格するまでの時間に 90-180 秒要する 」という NLB の内部仕様があるため、リスナーを経由した Blue 側ターゲットグループに紐づく Original 側タスクに対してすぐに到達できません。
しかし、プロダクション用及びテスト用リスナーから見ると、すでに Blue 側ターゲットグループを向いてしまっているため、両方とも通信エラー (無応答) が発生します。 実測ベースで言えば、約 70-80 秒通信できない期間が続いてしまいます。
安全に切り戻すことを期待していましたが、完全断が発生してしまう状況となってしまいます。
NLB ロールバックエラー発生に対する設計
ALB と比較して、NLB のロールバックは一時的にリクエストが完全断となってしまいました。 ここで根本的な要因としては、「リスナーから孤立したターゲットグループが再びリスナーから参照されるまでの時間 = 90-180秒 」という NLB の内部仕様です。 ここで裏を返せば、「 Blue/Green の動作によらず、ターゲットグループがリスナーから孤立しなければよい状況を作ればよい」ということになります。
そんなことができるのか、と思う方もいるかも知れません。 これは、必ずしもプロダクション用リスナー及びテスト用リスナーである必要はなく、ダミー用リスナーでもよいのです。 具体的には、以下のように設計することで、ロールバック時の完全断を回避することができます。
少々格好が悪い構成ですが、ロールバック時の NLB の挙動は次のように変わります。
正常デプロイ時と同様、アプリ疎通が切り替わるまでに ALB より時間がかかってしまう点は同じですが、無応答が回避できるので、より安全な設計といえます。 興味のある方はぜひ検証してみてください。
NLB のデプロイメント時エラーに関するフィードバック
さて、先程 NLB デプロイメント時における回避策として、CodeDeploy でルーティング前の猶予時間を設定する案を紹介し、これが暫定対応と述べました。 デプロイメント直前の NLB を振り返ると、以下のような状態でした。
この構成に関して、Green 側ターゲットグループが孤立しています。 つまり、今説明したロールバック時の通信エラーが発生してしまう根本原因と同じ状況ですね。
今回、ダミーリスナーを設定しました。 これにより、Green 側ターゲットグループもリスナーから孤立しない状況となっています。 そのため、必ずしもCodeDeployでルーティング前の猶予時間を設けなくても、デプロイメント時に発生した通信エラーの発生を抑制することができます。
言い換えれば、CodeDeploy のルーティング前の猶予時間はリリース前のテスト疎通要件があるかどうかで設定を判断すればよい、ということになります。
まとめ
今までの話をまとめると、
- ALB と NLB で Blue/Green デプロイメント時の振る舞いが異なる
- ALB は Replacement 側タスクが応答するまで 10-20 秒だが、NLB の場合は 60秒以上の時間がかかる
- NLB は適切に設計しないと、デプロイメント時およびロールバック時に通信エラーとなる可能性がある
- NLB ではダミーリスナーを設けることで、通信エラーを回避できる
という結論になります。
正直、NLB は玄人向けの AWS サービスです。 このトピック以外にも、ヘルスチェック設計やターゲットのセキュリティグループ設計など、ALB の感覚で設計するとうまく機能しなかったり、予想外の結果となる点が多数あります。
例えば、NLB のヘルスチェックは、コンセンサスメカニズムを利用しており、ALB と比較して非常に多くのヘルスチェックリクエストが飛びます。仮に HTTP ヘルスチェックを採用している場合、アプリケーションに対して予想以上の負荷がかかったり、ログが大量に出力されて料金が高くなる等の懸念も発生します。そのため、TCP ヘルスチェックの採用から検討すべき、など ALB とは異なる注意点があります。
Blue/Green デプロイメントを実装する場合、可能であれば ALB を採用したほうが切り替え・切り戻しの時間短縮を実現できますし、なによりシンプルに設計・運用できます。
一方、秒間数百万のリクエストをさばいたり、レイヤー 4 レベルでの負荷分散ができる点、PrivateLink として利用できる点はNLBならではの特徴です。 AWS が提供するロードバランサー間の特徴は Web 上で多数紹介されていますが、オフィシャルな整理内容としては以下を参照してみてください。
特徴 - Elastic Load Balancing | AWS
今回のダミーリスナーによる設計に関して、注意点としては NLB を internet-facing で作成すると、テストポートやダミーポートのリクエストも可能となってしまいます。 ユースケースによっては注意が必要ですが、internal で作成する場合や、APIGateway (REST API) + WAF --> (VPCリンク) --> NLB のような構成ではテストポートやダミーポートに対して無闇なアクセスを防げるため、有効な設計です。
NLB で Blue/Green デプロイメントを設計する際は、このブログ内容から「通信エラーを回避するためにはダミーリスナーが必要だったなぁ」と思い出していただけると嬉しいです。
※それ以前の話として、 NLB が ALB と同じような挙動にならないかなぁ、と思うばかりですが・・・AWS さん、何卒・・・🙏
だらだらと長くなってしまいましたが、最後までお読みいただきありがとうございました。 次回は、 「ECS 上で秘密情報を扱う際に利用される SSM パラメータストアと Secrets Manager 結局どういう考え方でどちらを使えばよいの?」という内容で投稿予定です😄 多分、ECS 利用している人は悩むポイントなので、その点共有できればなぁと考えています。
ではでは。