Implementing ECS native Blue/Green deployment with CDK L2 constructs
Recently, the ECS native Blue/Green deployment feature became available without requiring CodeDeploy.
The benefits include the ability to use ECS Service Connect together with Blue/Green deployment, and the ability to define Blue/Green deployments simply, which had tended to become complex in configuration.
I tried implementing this ECS Blue/Green deployment using CDK's L2 construct, which is now available.
The implementation details are described in the CDK repository's README.md.
import * as lambda from "aws-cdk-lib/aws-lambda";
declare const cluster: ecs.Cluster;
declare const taskDefinition: ecs.TaskDefinition;
declare const lambdaHook: lambda.Function;
declare const blueTargetGroup: elbv2.ApplicationTargetGroup;
declare const greenTargetGroup: elbv2.ApplicationTargetGroup;
declare const prodListenerRule: elbv2.ApplicationListenerRule;
const service = new ecs.FargateService(this, "Service", {
cluster,
taskDefinition,
deploymentStrategy: ecs.DeploymentStrategy.BLUE_GREEN,
});
service.addLifecycleHook(
new ecs.DeploymentLifecycleLambdaTarget(lambdaHook, "PreScaleHook", {
lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP],
})
);
const target = service.loadBalancerTarget({
containerName: "nginx",
containerPort: 80,
protocol: ecs.Protocol.TCP,
alternateTarget: new ecs.AlternateTarget("AlternateTarget", {
alternateTargetGroup: greenTargetGroup,
productionListener:
ecs.ListenerRuleConfiguration.applicationListenerRule(prodListenerRule),
}),
});
target.attachToApplicationTargetGroup(blueTargetGroup);
In the ECS Service definition, select BLUE_GREEN
as the deploymentStrategy
, and when adding to the target group using service.loadBalancerTarget
, specify the test listener and additional target groups.
By preparing two target groups, ECS nicely switches the targets that each listener forwards to.Therefore, it is important to note that you need to prepare an infrastructure role with the AWS managed policy AmazonECSInfrastructureRolePolicyForLoadBalancers.
The code example in the README.md mentioned above is simplified, so I implemented it including the associated resource definitions in the repository below.
CDK is set up to create the following architecture.
The CloudFormation generated by synth looks like this:
The Blue target group is properly specified as TargetGroupArn
, and the Green target group is properly specified as AdvancedConfiguration.AlternateTargetGroupArn
.
ServiceD69D759B:
Type: AWS::ECS::Service
Properties:
Cluster:
Ref: ClusterEB0386A7
DeploymentConfiguration:
BakeTimeInMinutes: 2
MaximumPercent: 200
MinimumHealthyPercent: 50
Strategy: BLUE_GREEN
DesiredCount: 1
EnableECSManagedTags: false
HealthCheckGracePeriodSeconds: 60
LaunchType: FARGATE
LoadBalancers:
- AdvancedConfiguration:
AlternateTargetGroupArn:
Ref: AppTargetGroupGreen5462D905
ProductionListenerRule:
Ref: AlbListenerRuleE27FB4D8
RoleArn:
Fn::GetAtt:
- InfrastructureRoleB0C76DC8
- Arn
TestListenerRule:
Ref: AlbTestListenerRule5FCB082B
ContainerName: app
ContainerPort: 80
TargetGroupArn:
Ref: AppTargetGroupBlueE8C741C5
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- Fn::GetAtt:
- AppSg898BCA4E
- GroupId
Subnets:
- Ref: VpcPrivateSubnet1Subnet536B997A
- Ref: VpcPrivateSubnet2Subnet3788AAA1
PlatformVersion: 1.4.0
TaskDefinition:
Ref: TaskDefinitionB36D86D9
```CloudFormation implementation is also referenced in the AWS official documentation, and I confirmed that it has the same configuration.
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/migrate-codedeploy-to-ecs-bluegreen-cloudformation-template.html
## Blue/Green Deployment Flow
I'll quote the diagram from the official documentation as it's easy to understand.

[Amazon ECS blue/green service deployments workflow](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/blue-green-deployment-how-it-works.html)
The flow is as follows:
1. Traffic flows from the production listener to the Blue side (left) target group
2. Register tasks with the new version to the Green side (right) target group, making them accessible via the test listener
3. After a certain period, change the production listener to also direct traffic to the Green side
ECS handles these switches nicely by calling the Modify Rule API.
There are always two target groups, but which target group receives requests at the start of deployment alternates with each deployment.
Additionally, it's possible to implement application health tests using Lambda functions at stages like POST_TEST_TRAFFIC_SHIFT (immediately after the new version of the application becomes accessible via the test listener).
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/blue-green-deployment-how-it-works.html#blue-green-deployment-stages## Let's Deploy
Let's give it a try.
After CDK deployment, when I send a request, I get the following response.
% curl IacSta-Alb16-c6mbkOEOUI88-826201051.ap-northeast-1.elb.amazonaws.com
Hello World v1
At this point, only the Blue target group has ECS tasks registered as targets.
**Target Group (Blue)**

**Target Group (Green)**

Both listeners are directing traffic to the Blue target group.
**Production Listener**

**Test Listener**

Let's modify the application and deploy it.
I'm only changing the string that's returned when accessing the root path.
https://github.com/masutaro99/cdk-ecs-blue-green/blob/51e75d5eb84119f3b40d65915517b1ccbfc3b7f4/packages/server/src/index.ts#L6
Since we're using [ContainerImage.fromAsset](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.ContainerImage.html), running cdk deploy after changing the application code will build the container image and then deploy it.
https://github.com/masutaro99/cdk-ecs-blue-green/blob/51e75d5eb84119f3b40d65915517b1ccbfc3b7f4/packages/iac/lib/iac-stack.ts#L59
I'll deploy with the following CDK diff.
Stack IacStack
Resources
[~] AWS::ECS::TaskDefinition TaskDefinition TaskDefinitionB36D86D9 replace
└─ [~] ContainerDefinitions (requires replacement)
└─ @@ -2,7 +2,7 @@
[ ] {
[ ] "Essential": true,
[ ] "Image": {
[-] "Fn::Sub": "
[+] "Fn::Sub": "
[ ] },
[ ] "LogConfiguration": {
[ ] "LogDriver": "awslogs",
% curl http://IacSta-Alb16-c6mbkOEOUI88-826201051.ap-northeast-1.elb.amazonaws.com:8080
Hello World v2
In terms of deployment stages, we've now reached the "after test traffic migration" stage.

Looking at CloudTrail, we can see that `ecs.amazonaws.com` is using the Modify Rule API to change the weights of listener rules that direct traffic to each target group.
```json
{
"eventVersion": "1.11",
"userIdentity": {
"type": "AssumedRole",
"principalId": "XXXXXXX:ECSNetworkingWithELB",
"arn": "arn:aws:sts::XXXXXXX:assumed-role/IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6/ECSNetworkingWithELB",
"accountId": "XXXXXXX",
"accessKeyId": "XXXXXXX",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "XXXXXXX",
"arn": "arn:aws:iam::XXXXXXX:role/IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6",
"accountId": "XXXXXXX",
"userName": "IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6"
},
"attributes": {
"creationDate": "2025-08-30T05:35:19Z",
"mfaAuthenticated": "false"
}
},
"invokedBy": "ecs.amazonaws.com"
},
"eventTime": "2025-08-30T05:35:19Z",
"eventSource": "elasticloadbalancing.amazonaws.com",
"eventName": "ModifyRule",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "ecs.amazonaws.com",
"userAgent": "ecs.amazonaws.com",
"requestParameters": {
"ruleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:listener-rule/app/IacSta-Alb16-c6mbkOEOUI88/58c0dd8224412063/ef249d1afd29053c/8e3689f373a18f80",
"actions": [
{
"type": "forward",
"forwardConfig": {
"targetGroups": [
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupBlue/d5a510d367b855d6",
"weight": 0
},
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupGreen/7617b6ca7679d5fa",
"weight": 100
}
],
"targetGroupStickinessConfig": {
"enabled": false
}
}
}
]
},
"responseElements": {
"rules": [
{
"ruleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:listener-rule/app/IacSta-Alb16-c6mbkOEOUI88/58c0dd8224412063/ef249d1afd29053c/8e3689f373a18f80",
"priority": "1",
"conditions": [
{
"field": "path-pattern",
"values": ["*"],
"pathPatternConfig": {
"values": ["*"]
}
}
],
"actions": [
{
"type": "forward",
"forwardConfig": {
"targetGroups": [
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupGreen/7617b6ca7679d5fa",
"weight": 100
},
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupBlue/d5a510d367b855d6",
"weight": 0
}
],
"targetGroupStickinessConfig": {
"enabled": false
}
}
}
],
"isDefault": false
}
]
},
"requestID": "f26ab8db-ed22-4531-9a13-056dc5c63e99",
"eventID": "3306d1b5-7699-42d6-9667-de0e0947d15c",
"readOnly": false,
"eventType": "AwsApiCall",
"apiVersion": "2015-12-01",
"managementEvent": true,
"recipientAccountId": "XXXXXXX",
"eventCategory": "Management"
}
```Initially, only the test listener was directing traffic to the Green target group.

After a short while, requests start flowing to the production side as well.
% curl http://IacSta-Alb16-c6mbkOEOUI88-826201051.ap-northeast-1.elb.amazonaws.com
Hello World v2
Checking the production listener configuration, we can see that it has also started directing traffic to the Green target group.

Looking at CloudTrail, we can confirm that `ecs.amazonaws.com` similarly called the Modify Rule API.
```json
{
"eventVersion": "1.11",
"userIdentity": {
"type": "AssumedRole",
"principalId": "XXXXXXX:ECSNetworkingWithELB",
"arn": "arn:aws:sts::XXXXXXX:assumed-role/IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6/ECSNetworkingWithELB",
"accountId": "XXXXXXX",
"accessKeyId": "XXXXXXX",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "XXXXXXX",
"arn": "arn:aws:iam::XXXXXXX:role/IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6",
"accountId": "XXXXXXX",
"userName": "IacStack-InfrastructureRoleB0C76DC8-RVjhoJmLJ3N6"
},
"attributes": {
"creationDate": "2025-08-30T05:36:00Z",
"mfaAuthenticated": "false"
}
},
"invokedBy": "ecs.amazonaws.com"
},
"eventTime": "2025-08-30T05:36:00Z",
"eventSource": "elasticloadbalancing.amazonaws.com",
"eventName": "ModifyRule",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "ecs.amazonaws.com",
"userAgent": "ecs.amazonaws.com",
"requestParameters": {
"ruleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:listener-rule/app/IacSta-Alb16-c6mbkOEOUI88/58c0dd8224412063/1d61e03731b12062/001283748f5a49e2",
"actions": [
{
"type": "forward",
"forwardConfig": {
"targetGroups": [
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupBlue/d5a510d367b855d6",
"weight": 0
},
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupGreen/7617b6ca7679d5fa",
"weight": 100
}
],
"targetGroupStickinessConfig": {
"enabled": false
}
}
}
]
},
"responseElements": {
"rules": [
{
"ruleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:listener-rule/app/IacSta-Alb16-c6mbkOEOUI88/58c0dd8224412063/1d61e03731b12062/001283748f5a49e2",
"priority": "1",
"conditions": [
{
"field": "path-pattern",
"values": ["*"],
"pathPatternConfig": {
"values": ["*"]
}
}
],
"actions": [
{
"type": "forward",
"forwardConfig": {
"targetGroups": [
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupGreen/7617b6ca7679d5fa",
"weight": 100
},
{
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXX:targetgroup/AppTargetGroupBlue/d5a510d367b855d6",
"weight": 0
}
],
"targetGroupStickinessConfig": {
"enabled": false
}
}
}
],
"isDefault": false
}
]
},
"requestID": "1056961e-b202-41ce-aca9-90e51585b878",
"eventID": "6431fa6d-572d-4196-b45a-9cba6f8405ce",
"readOnly": false,
"eventType": "AwsApiCall",
"apiVersion": "2015-12-01",
"managementEvent": true,
"recipientAccountId": "XXXXXXX",
"eventCategory": "Management"
}
```The ModifyRule was executed twice, once for the test listener and once for the production listener, with a time difference of about 40 seconds between them.
It's important to note that there is no setting to customize the time difference during the switch.
In ECS native Blue/Green deployments, there is a configurable wait time called "bake time," but this is the time to keep existing version tasks after traffic switching, not the time to wait for the production listener to switch.
> Bake time - The period during which both blue and green service revisions run simultaneously after production traffic has migrated.
> https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/deployment-type-blue-green.html
Therefore, to perform any testing when only the test listener has switched, you need to prepare a Lambda and set it as a lifecycle hook.
The decision flow for whether to roll back in Blue/Green deployments should be determined in advance, and this can be implemented as a Lambda. However, note that what could be simply configured in CodeDeploy is no longer available.
## Rollback
Since old tasks continue to live during the configured bake time, you can roll back by simply changing the listener settings.
If you execute a "rollback," ECS will execute Modify Rule for you, similar to deployment.

Nevertheless, it took almost 30 seconds from executing the rollback until the listener was rewritten, so it's ideal to properly organize tests as much as possible when only the test listener has switched.
