はじめに
ECSで2025年7月にBlue/Greenデプロイが、2025年10月にLinearデプロイとCanaryデプロイがCodeDeployを使わず、ECS組み込みの機能として使えるようになりました。
これにより、CodeDeployに関するリソースが不要になる、ServiceConnectに対応するなどの嬉しい点があったため、私が所属しているチームでは発表されたその週に導入しました。
これを試してみる系のブログはたくさんありますが、基本的にはCanaryデプロイかLinearデプロイかは二者択一になっています。
しかし、Canaryデプロイでは3%だけ公開したら残りの97%がいきなり全て公開されてしまう、Linearデプロイでは3%だけ公開して一旦しばらく様子を見るといったことができないという問題点があります。
より理想的なリリースとしてはCanaryデプロイで少しだけ公開してしばらく待ち、エラーログなどを監視して問題ないことを確認した後に、Linearデプロイで公開する割合を増やしていく方法のはずです。
やってみるとそれが意外と簡単に実現できたのでまとめました。
実現したこと
以下の3つを全て兼ね備えたデプロイ手法
- Blue/Greenデプロイ
- リリースする資材をクライアント公開する前に、本番環境でテストを行う
- Canaryデプロイ
- 本番環境のトラフィックの3%だけ新しい資材に流し、任意のタイミングで残りの97%のトラフィックを新しい資材に流す
- Linearデプロイ
- 97%のトラフィックを一気に新しい資材に変更するのではなく、数分ごとに新しい資材に流すトラフィックの割合を増やす
前提
アーキテクチャ
以下の構成のうち、重要なのはALBのターゲットグループをECSサービスにしているという点です。
- CloudFrontのオリジンをprivate subnetのALBにする
- VPCオリジンという去年追加された機能を使用しています。詳しくは以下のリンクから)
- public subnetのALBでもデプロイには影響ありません
- ALBのターゲットグループをECSサービスにする
- ECSサービス間の通信はService Connectを使用する
- ECS間の通信をServiceDiscovery(サービス検出)を使っている場合は対応されていないのでServiceConnectに変えてください
デプロイ構成
以下のデプロイ構成となっていますが、この構成でないといけないということは特にありません。
- ECSクラスター、ALBのリスナー・リスナールール・ターゲットグループ、ECSのタスク実行ロール・タスクロールなどIAMロールなどのAWSリソースはterraform管理
- ECSサービスはecspresso管理
- ecspressoではtfstate pluginを用いてAWSリソースのarn・idを指定する
- GitHub Actionsでecspresso deployを実行する
ecspressoに関する説明は以下など参考にしてください
実装
概要
以下に記載のある通り、ECSデプロイの指定したステージでLambdaを叩き、そのレスポンスに応じて次のステージに進むかどうかを制御することができます。
これを利用して、リニアデプロイを選択しつつ、テストトラフィックを移行した後と、リニアデプロイの最初の本番トラフィックが移行した後にデプロイを停止することで、リニアデプロイに加えてBlue/GreenデプロイとCanaryデプロイを実現します。
設定
- ECSのデプロイ戦略はリニアを選択する
- デプロイライフサイクルフックに「テストトラフィック移行後」のライフサイクルステージに対応するLambdaと「本番トラフィック移行」のライフサイクルステージに対応するLambdaを別々で作成し、設定する
- それぞれのLambdaでは環境変数の値をそのまま返すように実装する
- 以下のドキュメントに参考のPython関数があるので、レスポンスの値を環境変数から取得するように変更します
- https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/blue-green-deployment-how-it-works.html
- GitHub ActionsからLambdaの環境変数を変更できるように実装する
実際の流れ
- GitHub Actionsで2つのLambdaの環境変数を「IN_PROGRESS」に初期化する
- GitHub Actionsでecsressso deployコマンドを実行し、ECSのデプロイを開始する
- スケールアップなどのデプロイステージを経て、テストトラフィック移行後ステージになる
- 「テストトラフィック移行後」ステータスになるとライフサイクルフックのLambdaが叩かれる
- Lambdaは環境変数「IN_PROGRESS」をそのまま返す
- ECSは「IN_PROGRESS」が返ってきた場合は次のステージに進まず、一定時間後に再度Lambdaを叩くことを繰り返す
- この間に、テストリスナー(自分のプロダクトではポートを分けたくない理由があり、テスト用のリスナールール)を使用し、APIテストなどを実行する
- APIテストなどで失敗した場合、Lambdaの環境変数を「FAILED」とするとロールバックできる
- APIテストなどで成功した場合は、GitHubActionsでLambdaの環境変数を「SUCCEEDED」に変更する
- ECSのライフサイクルフックのレスポンスがSUCCEEDEDになることにより、デプロイステージが「本番トラフィック移行」に進む
- 「本番トラフィック移行」でも同様にライフサイクルフックを用意しているため、最初の3%だけ本番トラフィックが移行した状態でデプロイが停止する
- しばらくエラーレートなどを監視し、問題がないことを確認する
- 問題があった場合、「本番トラフィック移行」ステージに対応するLambdaの環境変数を「FAILED」とするとロールバックできる
- 「本番トラフィック移行」ステージに対応するLambdaの環境変数を更新することで再度デプロイが停止せず、次の3%のトラフィックが移行される
- ここで「本番トラフィック移行」のステージからは変わらないが、おそらくステージが切り替わりはするようで、3%の移行ごとにライフサイクルフックは叩かれる
- 最初の3%移行のライフサイクルフックではLambdaの環境変数は基本的に「SUCCEEDED」から変わらないため、停止せずに100%までデプロイが自動で進行する
- エラーなどが発生した場合はLambdaの環境変数を「FAILED」とするとロールバックできる
注意事項
Lambdaの環境変数を使用している都合上、同じLambdaを利用した複数のデプロイが同時に走ると事故が発生してしまいます。
デプロイする資材/環境ごとにLambdaを分けないといけないことに注意してください。
