ECS built-in blue/green deployments
従来、ECSでBlue/Greenデプロイメントを実現するにはCodeDeployやオレオレ実装が必要でしたが、
ECSサービス内にBlue/Greenデプロイが組み込まれました!🎉🎉🎉
従来のCodeDeployとIaC(特にCDK)の相性が以下の点で悪いと個人的に感じていました。
- CodeDeploy自体がリソースを変更するためドリフトが発生する
- CloudFormation(CFn)にはTerraformの
ignore_changes
相当の機能がない
- CloudFormation(CFn)にはTerraformの
- ServiceのネットワークまわりがCodeDeploy(appspec.yml)経由での変更が必要で、CDKから変更できなくなる
- CFnには
AWS::CodeDeploy::BlueGreen Hook
などがありましたが、CDKから扱いにくい
- CFnには
様々な黒魔術で対応していた方が多かったと思いますが、今回のアップデートでもしかして!と思い、CDK × ECS Blue/Greenを試してみた次第です。
参考資料/記事
-
素晴らしい記事なので最初に読むのをオススメします!
ECSのネイティブBlue/Greenが登場したので検証!フック・Dark Canary・コントローラ更新も強力 (t-kikuc) - 公式ドキュメント
Let's CDK
built-in blue/green 対応状況(2025/07/20現在)
- CFn: ✅ 対応済み
- CDK L1: ✅ 実装済み
- CDK L2: 🚧 これから( https://github.com/aws/aws-cdk/issues/35010 )
というわけで、L2で作りつつescape hatchを利用してCDKで実装してみることにしました。
要件
- ALB × ECS(Fargate)でHTTP経由のHello World
Code
早速ですが、コードはこんな感じです。
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";
export class ECSBlueGreenSampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "VPC", {
maxAzs: 2,
natGateways: 0,
ipAddresses: ec2.IpAddresses.cidr("10.128.0.0/16"),
restrictDefaultSecurityGroup: false,
});
const cluster = new ecs.Cluster(this, "Cluster", {
vpc,
});
const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef");
taskDef.addContainer("web", {
image: ecs.ContainerImage.fromRegistry("nginxdemos/hello"),
cpu: 256,
memoryLimitMiB: 512,
portMappings: [{ containerPort: 80 }],
});
const serviceSg = new ec2.SecurityGroup(this, "ServiceSG", {
vpc,
allowAllOutbound: true,
});
serviceSg.addIngressRule(
ec2.Peer.ipv4("10.128.0.0/16"),
ec2.Port.tcp(80),
"Allow HTTP traffic"
);
const service = new ecs.FargateService(this, "Service", {
cluster,
taskDefinition: taskDef,
deploymentController: {
type: ecs.DeploymentControllerType.ECS,
},
securityGroups: [serviceSg],
assignPublicIp: true,
enableExecuteCommand: true, // for debugging
});
const targetGroupA = new elbv2.ApplicationTargetGroup(
this,
"TargetGroupA",
{
vpc,
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
targetType: elbv2.TargetType.IP,
}
);
const targetGroupB = new elbv2.ApplicationTargetGroup(
this,
"TargetGroupB",
{
vpc,
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
targetType: elbv2.TargetType.IP,
}
);
const albSg = new ec2.SecurityGroup(this, "AlbSG", {
vpc,
allowAllOutbound: true,
});
albSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
"Allow HTTP Prod traffic"
);
albSg.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(8080),
"Allow HTTP Test traffic"
);
const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
vpc,
internetFacing: true,
securityGroup: albSg,
});
const prodListener = alb.addListener("ProdListener", {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
open: true,
defaultAction: elbv2.ListenerAction.fixedResponse(500),
});
const prodListenerRule = new elbv2.ApplicationListenerRule(
this,
"ProdListenerRule",
{
listener: prodListener,
priority: 1,
conditions: [elbv2.ListenerCondition.pathPatterns(["/*"])],
// NOTE: 以下はECS Deploymentによって変更されるためドリフトする点に留意
// CustomResourceなどで初回以降はCFnの管理外に置いて、ECS Deployment側の変更を尊重した方が安全かも
action: elbv2.ListenerAction.weightedForward([
{ targetGroup: targetGroupA, weight: 1 },
{ targetGroup: targetGroupB, weight: 0 },
]),
}
);
const testListener = alb.addListener("TestListener", {
port: 8080,
protocol: elbv2.ApplicationProtocol.HTTP,
open: true,
defaultAction: elbv2.ListenerAction.fixedResponse(500),
});
const testListenerRule = new elbv2.ApplicationListenerRule(
this,
"TestListenerRule",
{
listener: testListener,
priority: 1,
conditions: [elbv2.ListenerCondition.pathPatterns(["/*"])],
// NOTE: 以下はECS Deploymentによって変更されるためドリフトする点に留意
// CustomResourceなどで初回以降はCFnの管理外に置いて、ECS Deployment側の変更を尊重した方が安全かも
action: elbv2.ListenerAction.weightedForward([
{ targetGroup: targetGroupA, weight: 1 },
{ targetGroup: targetGroupB, weight: 0 },
]),
}
);
// escape hatch for ECS Service
// ref. https://github.com/aws/aws-cdk/issues/35010
// ref. https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/cfn-layer.html
const serviceCf = service.node.defaultChild as ecs.CfnService;
serviceCf.loadBalancers = [
{
targetGroupArn: targetGroupA.targetGroupArn,
containerName: "web",
containerPort: 80,
advancedConfiguration: {
alternateTargetGroupArn: targetGroupB.targetGroupArn,
// ALBの場合はlistenerではなく、listenerRuleを指定する点に留意
productionListenerRule: prodListenerRule.listenerRuleArn,
testListenerRule: testListenerRule.listenerRuleArn,
// ref. https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AmazonECSInfrastructureRolePolicyForLoadBalancers.html
roleArn: new iam.Role(this, "BlueGreenRole", {
assumedBy: new iam.ServicePrincipal("ecs.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"AmazonECSInfrastructureRolePolicyForLoadBalancers"
),
],
// ドキュメントに記載が無いが以下ポリシーも必要
inlinePolicies: {
DescribeTargets: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
],
resources: ["*"],
}),
],
}),
ModifyTargetGroup: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DeregisterTargets",
],
resources: [
targetGroupA.targetGroupArn,
targetGroupB.targetGroupArn,
],
}),
],
}),
},
}).roleArn,
},
},
];
serviceCf.deploymentConfiguration = {
strategy: "BLUE_GREEN",
};
}
}
const app = new cdk.App();
new ECSBlueGreenSampleStack(app, 'ECSBlueGreenSampleStack', {});
実装時の課題
1. CFnの制限への対応
課題: CFnではListenerのdefault actionのARNが取得できない
対処法: そのためdefault actionは500エラーを返すように設定し、別途Listener Ruleを作成して処理する形を取りました。
2. リソースのドリフト問題
課題: Listener RuleがECSデプロイメント時に変更され、CDKコードとドリフトする
- ECS Deployment側が変更したListener RuleをCDKから変更してしまうと、タイミングによってはターゲットがいなくなる障害が発生する可能性。
- IaC理念的に気持ち悪さあり。
対処法(案):
- CustomResourceなどで初回以外は変更できなくするほうが安全?
- ECS Deployment側の変更を尊重した方が良いと思う
それでもネットワークまわりの設定が宣言できるようになったのはめちゃくちゃありがたい!
3. IAMロールの追加権限
課題: ドキュメントにはマネージドポリシー AmazonECSInfrastructureRolePolicyForLoadBalancers
を指定すれば行けるような書き方だが、実際には追加でポリシーが必要
必要Action (いずれもリソース指定不可のActionです)
elasticloadbalancing:DescribeTargetGroups
elasticloadbalancing:DescribeTargetHealth
elasticloadbalancing:RegisterTargets
elasticloadbalancing:DeregisterTargets
(めちゃくちゃハマったんですがt-kikucさんの記事に助けられました、ありがとうございます🙏)
まとめ
ドリフトの課題は完全に解消されなかったものの、今まで試したCDK × ECS Blue/Greenで最もシンプルに書けたのは間違いないです。
従来のCodeDeployベースの実装と比較して
- 設定がめちゃくちゃシンプルになった、知るべき概念が減った
- CDKでネットワーク設定を宣言的に管理できる
- ECS内で完結するため、運用の複雑さが軽減
- CI/CDもシンプルに構成できそう
個人的にはAWS 2025年一番の神アップデートでした!
Deployment Lifecycle Hook
今回は極力シンプルにするためDeployment Lifecycle Hookを利用していませんが、かなりプリミティブなAPIなので、組織のルール・文化に合わせた柔軟なデプロイフローを組むなんて事もできると思います!
感想
正式にL2が出るまではプロダクションで使うのは少々抵抗がありますが、それでも今からCodeDeployで構成する手はもう無いかな、という所感です。改めて神アプデ!
同じ悩みをお持ちの方がいれば是非一度試してみてほしいです!
それでは、良いCDKライフを!