ECSネイティブのBlue/Greenデプロイ構築のポイントとDurable Functionsを使ったライフサイクルフックの実装
こんにちは。エンジニアマネージャーのkiwiです。
2025年7月17日に、ECSの組み込みデプロイ戦略としてBlue/Greenデプロイが利用可能となりました。従来、CodePipelineからECSへBlue/Greenデプロイを行うにはCodeDeployを経由する必要がありました。このアップデートにより、CodeDeploy不要でECSネイティブなBlue/Greenデプロイが可能になりました。

ロードバランサー経由の場合、テストフェーズではALBの加重ルーティングの仕組みを使い、テスト用トラフィックのみがGreenのターゲットグループに流れるようになります。
本番トラフィックを切り替えるフェーズで、BlueとGreenの加重設定が逆転し、本番トラフィックが新バージョンに流れるようになります。
従来のCodeDeployを使った方式の環境構築は経験があったのですが、今回上記の環境をCloudFormationを使って構築する機会がありました。その際に気付いたポイントと、Durable Functionsを使ったライフサイクルフック(テスト後の手動承認フロー)の実装を紹介します。
目次
主な作成リソースとポイント
VPCやサブネット、セキュリティグループなどネットワーク周りについては従来の手法と変わらないため割愛し、Blue/Greenデプロイのポイントとなる「ターゲットグループ」「ロードバランサーとルーティング」「IAM Role」に絞って紹介します。
ターゲットグループ
Blue(本番稼働)とGreen(テスト稼働)2つのターゲットグループを用意する必要があります。この点はこれまでと変わりません。
実際に本番トラフィックがどちらのターゲットグループに流れるかはデプロイ試行ごとに切り替わります。ターゲットグループのリソース名としてBlueやGreenを使わず、A/Bなど切り替わっても良い名前にしておくと良いと思います。
テスト用のALBリスナー
CodeDeployを利用する場合、本番稼働とテスト稼働の2つのポートを指定してリスナールールを作成していました。それぞれのリスナーで単一のターゲットグループを指定しておき、デプロイのたびにリスナーからルーティングされるターゲットグループが即座に切り替わる挙動でした。
ECSのBlue/Greenデプロイでは、同じくリスナールールを2つ使うのですが、作成済みのリスナールールを使う場合、BlueとGreenのターゲットグループ両方にアクセスできるよう、加重ルーティングとして設定しておく必要があります。

また、ECSのコンソールからサービスを作成する場合、パスを使ったルーティングルールが作成できます。パス以外の条件で振り分けたい場合は、本番用と同じリスナーで事前にリスナールールを作成しておく必要があります。この時、加重ルーティング設定にしておかないと、コンソールから選択できないため注意してください。
ポート番号を指定してテスト用トラフィックを受け入れられるようにする場合、リスナールールだけでは実現できないため、ポートを指定した別のリスナーを用意して指定する必要があります。構成が少し複雑になるため、セキュリティグループを使ってテスト用トラフィックのアクセス元をポート番号で絞るケースなどに限って利用すると良いでしょう。

テスト用トラフィックを流す専用リスナーを作成した場合の例
まとめると、
- ルーティングに使用するロードバランサーを用意する
- ロードバランサーに本番用トラフィックが流れるリスナールールを作成する
- Blue用、Green用の2つのターゲットグループに加重ルーティングする設定にしておく
- ロードバランサーにテスト用トラフィックが流れるリスナールールを作成する
- ポート番号を分ける場合
- リスナーを別途作成する
- ポート番号を分けない場合
- 本番用と同じリスナーを使用する
- パスやヘッダーなど任意の条件でルーティングするルールを作成する
- リスナールールの優先度を本番よりも高くしておく
- いずれの場合も本番用トラフィックと同じく、Blue用、Green用の2つのターゲットグループに加重ルーティングする設定にしておく
- ポート番号を分ける場合
以上の準備(または構築)をした上で、ECSサービスを作成していきます。ターゲットグループとそのリスナールールをCloudFormationで構築する場合のテンプレート例は以下の通りです。(振り分け条件など一部省略しています)
# タスクを動作させるターゲットグループ(B/G用1)
TaskTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 80
Protocol: HTTP
TargetType: ip
VpcId: !Ref VpcId
HealthCheckPath: '/'
HealthCheckProtocol: HTTP
HealthCheckPort: 8080
HealthCheckIntervalSeconds: 60
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
Matcher:
HttpCode: 200
TaskLoadBalancingListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TaskTargetGroup
Type: forward
Conditions:
- (省略)
ListenerArn: !Ref Listener
Priority: !Ref LBPriority1
# タスクを動作させるターゲットグループ(B/G用2)
TaskTargetGroupAlt:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 80
Protocol: HTTP
TargetType: ip
VpcId: !Ref VpcId
HealthCheckPath: '/'
HealthCheckProtocol: HTTP
HealthCheckPort: 8080
HealthCheckIntervalSeconds: 60
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
Matcher:
HttpCode: 200
TaskLoadBalancingListenerRuleAlt:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TaskTargetGroupAlt
Type: forward
Conditions:
- (省略)
ListenerArn: !Ref ListenerAlt
Priority: !Ref LBPriority2
ECSがロードバランサーの設定を変更するためのIAM Role
ECSによるデプロイでは、ECSサービス自身がBlue/Greenのトラフィックを切り替えるために必要なRoleを指定する必要があります。(ドキュメント)

必要となる権限はAmazonECSInfrastructureRolePolicyForLoadBalancersというマネージドポリシーにまとめられています。このポリシーを持つECS向けのRoleを作成し、サービス作成時に紐付けます。
なお、上記のマネージドポリシーは同一アカウント内のすべてのLBの設定が変更可能な権限です。必要に応じて範囲を制限するなどのカスタマイズを行なってください。
手動テスト実行後に本番トラフィックを切り替える方法
CodeDeployによるBlue/Greenデプロイでは、Green環境が構築されテスト用トラフィックが処理できるようになった後、本番トラフィックを新バージョンに切り替えるまでの時間を指定することができました。この時間を使って最終テストを行い、問題なければデプロイ画面から本番トラフィックを切り替えたり、問題があればデプロイを中止(またはBlue環境への切り戻し)を行っていたと思います。
ECSによるBlue/Greenデプロイではそのような時間の設定はできないため、テストトラフィックが流れるようになった後すぐに本番トラフィックへの切り替えが走ります。そのため、通常の設定のみでは切り替え前にテストを実行することができません。

テスト完了までデプロイを一時停止したい場合、ECSのデプロイではライフサイクルフックを使ってLambda関数を実行します。デプロイ中の特定のタイミングでLambda関数を呼び出し、関数が成功の応答を返すまでデプロイを一時停止する仕組みです。
関数を起動できるタイミングはいくつかありますが(ドキュメント)、上記のGreen環境でのテストを実行する場合は POST_TEST_TRAFFIC_SHIFT フックを使います。
公式ブログでは、Lambdaの中で簡単な外形テストを実施し、問題なければ成功の応答を返すように実装されています。テスト後に手動で本番トラフィックを切り替える場合、現在はDurable Functionsを使うのが適切です。
簡単な例として、15分のタイムアウトまでに承認が実行されたら成功ステータスを返却するLambdaの例が以下の通りです。
import { withDurableExecution } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(async (event, context) => {
try {
const [promise, callbackId] = await context.createCallback("approval", {
timeout: { minutes: 15 },
});
context.logger.info(`Waiting for approval callback (ID: ${callbackId})`)
const approval = await promise;
return {
hookStatus: "SUCCEEDED"
};
} catch (e) {
console.error(e);
return {
hookStatus: "FAILED",
};
}
});
主な注意点は以下の通りです。
- Durable Functionsを指定して実行するには、Durable Functions自体のバージョン(またはエイリアス)を指定する必要があります。
- ECSのライフサイクルフックからLambdaを実行する場合、ECSがLambdaを呼び出すためのRoleが必要です(ドキュメント)。関数バージョン(またはエイリアス)を付与して権限を与える必要があります。
- ECSのライフサイクルフックはLambdaを同期呼び出しするため、Durable Functionsの実行時間が15分を超えるものは呼び出すことができません。
ECSのライフサイクルフックに指定してデプロイを実行すると、指定したフックのタイミングでLambdaが起動します。Durable Functionsの実行履歴はLambdaの「永続実行」タブで見ることができます。

実行中の履歴のリンクをクリックすると現在のステップが確認できます。
承認が必要なフェーズでは、下記のようにアクションが実行可能となります。「送信成功」を選択するとデプロイが進行します。

もちろん、Slack等の外部システムと連携して、コールバックを受け付ける仕組みを整えるのが理想です。
ECS Service を作成する CloudFormation テンプレート
Blue/Greenデプロイを採用したECS Serviceを作成するテンプレート例は以下の通りです。なお、ここにはライフサイクルフックの設定は含めていません。
本番トラフィックを切り替えた後、リリース前の環境を維持する(ECS画面から切り戻し可能にする)時間は BakeTimeInMinutes で指定します。
# ECS Service
WebService:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref Cluster
DeploymentConfiguration:
Strategy: BLUE_GREEN
BakeTimeInMinutes: !Ref BakeTimeInMinutes
DesiredCount: !Ref DesiredCount
HealthCheckGracePeriodSeconds: 30
LaunchType: FARGATE
LoadBalancers:
- ContainerName: !Ref ContainerName
ContainerPort: 8080
TargetGroupArn: !Ref TaskTargetGroup
AdvancedConfiguration:
AlternateTargetGroupArn: !GetAtt TaskTargetGroupAlt.TargetGroupArn
ProductionListenerRule: !Ref TaskLoadBalancingListenerRule
TestListenerRule: !Ref TaskLoadBalancingListenerRuleAlt
RoleArn: !GetAtt BlueGreenDeployServiceRole.Arn
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- (省略)
Subnets:
- (省略)
SchedulingStrategy: REPLICA
TaskDefinition: !Ref TaskDefinition
CodePipelineに指定するデプロイステージ
CodePipelineのデプロイ方法としてECSのデプロイ機能を使う場合の、デプロイステージの定義例です。アクションのProviderにECSを指定するだけです。
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: ECS
Version: 1
Configuration:
ClusterName: !Ref Cluster
ServiceName: !Ref ServiceName
FileName: imageDetail.json
InputArtifacts:
- Name: BuildArtifact
まとめ
前回CodeDeployのBlue/Greenデプロイを構築した時は、まだCloudFormationでの構築ができず、ServiceとCodeDeployアプリケーションをコンソールから直接作成しないといけない時代でした。
ECSのBlue/Greenデプロイを構築するにあたり、従来の方法と違う部分が多くなっていましたが、ライフサイクルフックによる自動テストなどが可能となるなど、進化している部分も多いと感じました。
後半では手動でテストを行う場合の方法も紹介しましたが、今後は最低限の自動テストのみで自動デプロイを実行するように変更するなど、安全かつ省力でリリースを実施できる環境を整えていきたいと思います。
掲載内容は、記事執筆時点の情報をもとにしています。