AWS Builder Center
ECS Deployment Strategies: CodeDeploy Blue/Green vs. Native Canary and Linear

ECS Deployment Strategies: CodeDeploy Blue/Green vs. Native Canary and Linear

Deploy the same ECS Fargate application using CodeDeploy blue/green and ECS-native blue/green, compare lifecycle hooks, traffic shifting strategies, and rollback mechanics side by side, then use a decision framework to choose the right approach for your workload.

Developer Advocate @ AgilityFeat & WebRTC.ventures
You're running a production service on ECS and need safe deployments — the ability to validate a new version before real users see it, and instant rollback if something goes wrong. ECS offers three deployment strategies, each with a different trade-off between simplicity and safety:
  • The rolling update is the default — ECS launches new tasks, waits for health checks, then drains old ones. Simple, but it provides no instant rollback and no test traffic validation.
  • CodeDeploy blue/green is the original advanced option — full lifecycle hooks, a test listener for pre-production validation, and alarm-based rollback. It works, but requires setting up a separate CodeDeploy application, deployment group, AppSpec file, and service role.
  • ECS-native blue/green (launched July 2025) provides the same capabilities — Lambda lifecycle hooks, test listeners, canary/linear traffic shifting — with significantly less configuration and tighter ECS integration.
This post deploys the same application using both CodeDeploy and ECS-native approaches, compares the mechanics, and provides a decision framework for choosing between them.

Prerequisites

To follow along, you'll need:
  •  AWS CLI v2  configured with credentials that have permissions for ECS, EC2, ELB, IAM, CodeDeploy, CloudWatch, and Lambda
  • An AWS account — estimated cost is ~$1–2 USD (Fargate tasks running for a few hours, ALB)
  • A VPC with at least two public subnets in different Availability Zones (the default VPC works)
All lab files (CloudFormation templates, task definitions) are in the  companion repository . Clone it to follow along:
git clone https://github.com/nuxero/ecs-deployment-strategies-lab.git
cd ecs-deployment-strategies-lab

ECS Deployment Concepts

Before diving into strategies, a few concepts that both approaches share:
Task definition is a blueprint for your application. It specifies the container image, CPU/memory, port mappings, environment variables, and logging configuration. Each change to a task definition creates a new numbered revision (e.g.,  my-app:1 ,  my-app:2 ). Deploying = pointing a service at a new revision.
Service maintains a desired number of running tasks from a task definition. It handles scheduling, health checks, and integration with load balancers. The service's deployment controller determines how updates roll out.
Deployment controller determines who manages deployments:  ECS  (rolling update or native blue/green/canary/linear),  CODE_DEPLOY  (CodeDeploy-managed), or  EXTERNAL .
Task sets are groups of tasks within a service. Blue/green deployments maintain two task sets simultaneously — the blue (current) set serving production traffic, and the green (replacement) set being validated.
Target groups are how an ALB routes traffic to tasks. Two target groups enable blue/green switching — one for the current tasks, one for the replacement.
Production listener routes real user traffic (typically port 80/443).
Test listener routes to the replacement task set on a separate port (e.g., 8080). This allows you to validate the new version before any real users see it.
Fargate is the serverless compute engine for ECS. You define CPU and memory in the task definition; AWS manages the underlying infrastructure. All examples in this post use Fargate.
Diagram showing an Application Load Balancer with two listeners: a production listener on port 80 routing to the blue target group (current v1 tasks) and a test listener on port 8080 routing to the green target group (replacement v2 tasks).


Approach 1 — CodeDeploy Blue/Green

Prerequisites — CloudFormation Template

This template provisions the full infrastructure: an ECS Fargate service running a simple web application that returns "Hello World v1", an ALB with two target groups and two listeners (production on port 80, test on port 8080), and all CodeDeploy resources including a lifecycle hook Lambda function.
The key resources and their purpose:
Resource
Purpose
ECS Cluster + Service
Fargate service with  CODE_DEPLOY  deployment controller
ALB + 2 Target Groups
Blue/green traffic switching via production and test listeners
CodeDeploy Application + Deployment Group
Manages blue/green lifecycle and traffic shifting
Lambda Function
 AfterAllowTestTraffic  hook that validates the green task set via the test listener
The full template is in the companion repo. Here are the key sections that make it a CodeDeploy blue/green deployment — the parts that differ from a standard ECS service:
# ECS Service — deployment controller set to CODE_DEPLOY
Service:
Type: AWS::ECS::Service
DependsOn: ProductionListener
Properties:
ServiceName: deploy-lab-service
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 2
LaunchType: FARGATE
# CODE_DEPLOY deployment controller — CodeDeploy manages blue/green
DeploymentController:
Type: CODE_DEPLOY
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets: !Ref SubnetIds
SecurityGroups: [!Ref TaskSecurityGroup]
LoadBalancers:
- ContainerName: app
ContainerPort: 80
TargetGroupArn: !Ref BlueTargetGroup

# CodeDeploy deployment group — wires together service, target groups, listeners
DeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployApplication
DeploymentGroupName: ecs-deploy-lab-dg
ServiceRoleArn: !GetAtt CodeDeployServiceRole.Arn
DeploymentConfigName: CodeDeployDefault.ECSAllAtOnce
DeploymentStyle:
DeploymentType: BLUE_GREEN
DeploymentOption: WITH_TRAFFIC_CONTROL
ECSServices:
- ClusterName: !Ref Cluster
ServiceName: !GetAtt Service.Name
LoadBalancerInfo:
TargetGroupPairInfoList:
- TargetGroups:
- Name: ecs-deploy-lab-blue
- Name: ecs-deploy-lab-green
ProdTrafficRoute:
ListenerArns:
- !Ref ProductionListener
TestTrafficRoute:
ListenerArns:
- !Ref TestListener
# Keep blue task set alive for 5 minutes after traffic shifts (rollback window)
BlueGreenDeploymentConfiguration:
TerminateBlueInstancesOnDeploymentSuccess:
Action: TERMINATE
TerminationWaitTimeInMinutes: 5
DeploymentReadyOption:
ActionOnTimeout: CONTINUE_DEPLOYMENT
WaitTimeInMinutes: 0
The template also includes a Lambda function ( CodeDeployHook_ecs-deploy-lab-test-traffic ) that acts as the  AfterAllowTestTraffic  hook. It hits the test listener URL and reports success or failure back to CodeDeploy:
// AfterAllowTestTraffic hook — validates the green task set via test listener
exports.handler = async (event) => {
const deploymentId = event.DeploymentId;
const lifecycleEventHookExecutionId = event.LifecycleEventHookExecutionId;
let status = "Failed";

try {
// Wait for ALB to finish routing test listener to green target group
await new Promise(resolve => setTimeout(resolve, 10000));

// Hit the test listener to validate the green task set
const response = await fetch(process.env.TEST_URL);
const body = await response.text();
if (response.ok) {
console.log("Test traffic validation passed — HTTP", response.status);
console.log("Response from green task set:", body.trim());
status = "Succeeded";
}
} catch (err) {
console.error("Test traffic validation error:", err);
}

// Report result to CodeDeploy
await codedeploy.send(new PutLifecycleEventHookExecutionStatusCommand({
deploymentId, lifecycleEventHookExecutionId, status,
}));
};
Deploy the stack and wait for the service to stabilize:
aws cloudformation deploy \
--template-file ecs-codedeploy-prerequisites.yaml \
--stack-name ecs-codedeploy-lab \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
VpcId=<YOUR_VPC_ID> \
SubnetIds=<SUBNET_1>,<SUBNET_2>
Store outputs for later use:
ALB_DNS=$(aws cloudformation describe-stacks --stack-name ecs-codedeploy-lab \
--query 'Stacks[0].Outputs[?OutputKey==`ALBDns`].OutputValue' --output text)

# Verify v1 is serving traffic
curl http://$ALB_DNS
# Expected: Hello World v1

Deploy v2 with CodeDeploy

To trigger a blue/green deployment, register a new task definition revision and create a CodeDeploy deployment referencing it. The new task definition changes  APP_VERSION  from  1  to  2 . This makes the application respond with "Hello World v2":
Note: In a real project, deploying a new version means building a Docker image, pushing it to Docker registry (i.e. ECR, DockerHub, etc), and updating the  image  field in the task definition. This lab uses an inline  node -e  command with an environment variable to simulate version changes without requiring a Docker build workflow — keeping the focus on deployment mechanics.
  task-definition-v2.json   — identical to v1 except  APP_VERSION  changes from  "1"  to  "2" . Grab it from the companion repo ( codedeploy/task-definition-v2.json ).
Fill in the execution role ARN and register it:
# Inject the execution role ARN into the JSON file
EXEC_ROLE=$(aws iam get-role --role-name ecs-deploy-lab-execution-role \
--query 'Role.Arn' --output text)

sed -i "s|<EXECUTION_ROLE_ARN>|$EXEC_ROLE|" task-definition-v2.json

# Register the new task definition revision
NEW_TASK_DEF=$(aws ecs register-task-definition \
--cli-input-json file://task-definition-v2.json \
--query 'taskDefinition.taskDefinitionArn' --output text)

echo "New task definition: $NEW_TASK_DEF"

The AppSpec File for ECS

The ECS AppSpec declares which task definition to deploy and how to wire it to the load balancer. The  Hooks  section references Lambda functions to run at each lifecycle stage. Here's the structure:
# AppSpec structure for ECS CodeDeploy deployments
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
# The new task definition ARN to deploy
TaskDefinition: "arn:aws:ecs:us-east-1:123456789:task-definition/ecs-deploy-lab:2"
LoadBalancerInfo:
ContainerName: "app" # Must match container name in task definition
ContainerPort: 80 # Must match container port mapping
Hooks:
# Runs after test listener routes to green — validate before production shift
- AfterAllowTestTraffic: "CodeDeployHook_ecs-deploy-lab-test-traffic"
The available lifecycle hooks for ECS CodeDeploy deployments, in execution order:
  1. BeforeInstall — before the replacement task set is created
  1. AfterInstall — after the replacement task set is created and tasks are healthy
  1. AfterAllowTestTraffic — after the test listener routes to the green task set (this is where you validate)
  1. BeforeAllowTraffic — before production traffic shifts to green
  1. AfterAllowTraffic — after production traffic is fully on green
 AfterAllowTestTraffic  is the most important hook — it's your chance to validate the new version using real infrastructure (ALB, networking, container config) without affecting production users.
Now create the deployment, passing the AppSpec inline with the  $NEW_TASK_DEF  variable from the previous step:
Note: In a real project, you'd store the AppSpec in S3 and reference it with  --s3-location bucket=...,key=appspec.yaml,bundleType=YAML . We're passing it inline here to keep the lab self-contained.
# Create the deployment with inline AppSpec referencing $NEW_TASK_DEF
DEPLOY_ID=$(aws deploy create-deployment \
--application-name ecs-deploy-lab \
--deployment-group-name ecs-deploy-lab-dg \
--revision '{"revisionType": "AppSpecContent", "appSpecContent": {"content": "{\"version\": 0.0, \"Resources\": [{\"TargetService\": {\"Type\": \"AWS::ECS::Service\", \"Properties\": {\"TaskDefinition\": \"'"$NEW_TASK_DEF"'\", \"LoadBalancerInfo\": {\"ContainerName\": \"app\", \"ContainerPort\": 80}}}}], \"Hooks\": [{\"AfterAllowTestTraffic\": \"CodeDeployHook_ecs-deploy-lab-test-traffic\"}]}"}}' \
--query 'deploymentId' --output text)

echo "Deployment: $DEPLOY_ID"
Watch the deployment progress through each lifecycle stage:
# Poll deployment status
while true; do
STATUS=$(aws deploy get-deployment --deployment-id $DEPLOY_ID \
--query 'deploymentInfo.status' --output text)
echo "$(date +%H:%M:%S) $STATUS"
if [ "$STATUS" != "InProgress" ] && [ "$STATUS" != "Created" ]; then break; fi
sleep 10
done
Once the deployment succeeds, check the hook's CloudWatch Logs to confirm it validated the new version via the test listener:
# Fetch the most recent log stream from the hook function
aws logs tail /aws/lambda/CodeDeployHook_ecs-deploy-lab-test-traffic --since 10m
You should see output like:
INFO Test traffic validation passed — HTTP 200
INFO Response from green task set: Hello World v2
This log proves the test listener was routing to the green task set during  AfterAllowTestTraffic . The hook invoked port 8080, received "Hello World v2" from the new tasks, and reported  Succeeded  — triggering the production traffic shift.
After the deployment completes, production traffic (port 80) now serves v2:
curl http://$ALB_DNS
# Expected: Hello World v2
The deployment sequence:
  1. Green task set spins up with the new task definition
  1. Tasks register in the green target group and pass ALB health checks
  1. CodeDeploy switches test listener (port 8080) to the green target group
  1.  AfterAllowTestTraffic  hook fires → Lambda hits port 8080, confirms "Hello World v2"
  1. Hook reports  Succeeded  → production listener (port 80) shifts to green
  1. 5-minute termination wait → blue task set terminates
Note: In our testing, the ALB listener switch (step 3) takes a few seconds to propagate before traffic actually routes to the new target group. CodeDeploy marks the switch as complete and fires the hook immediately, but the ALB hasn't finished routing yet. This is a  known ALB behavior  — changes take a few seconds to propagate. The hook includes a 10-second wait to account for this. In production hooks running full test suites, the setup time before the first HTTP request should naturally cover this delay.

Traffic Shifting Options with CodeDeploy

The deployment configuration controls how production traffic shifts from blue to green after hooks pass:
  •  CodeDeployDefault.ECSAllAtOnce  — instant switch. All traffic moves to green in one step.
  •  CodeDeployDefault.ECSCanary10Percent5Minutes  — 10% to green for 5 minutes. If no alarms fire, remaining 90% shifts.
  •  CodeDeployDefault.ECSCanary10Percent15Minutes  — same but with 15-minute observation window.
  •  CodeDeployDefault.ECSLinear10PercentEvery1Minute  — 10% every minute over 10 minutes.
  •  CodeDeployDefault.ECSLinear10PercentEvery3Minutes  — 10% every 3 minutes over 30 minutes.
All options still use the test listener and hooks for pre-validation. The traffic shifting strategy only affects what happens after hooks succeed.
To change the strategy, update the deployment group:
# Switch to canary — 10% for 5 minutes, then the rest
aws deploy update-deployment-group \
--application-name ecs-deploy-lab \
--current-deployment-group-name ecs-deploy-lab-dg \
--deployment-config-name CodeDeployDefault.ECSCanary10Percent5Minutes

Automatic Rollback

You can attach CloudWatch alarms to the deployment group (via  alarm-configuration ). If an alarm fires during the deployment — whether during the canary/linear traffic shifting phase or right after the full switch — CodeDeploy stops the deployment and switches the production listener back to the blue target group. This is instant because the blue task set remains alive during the termination wait period (5 minutes in our template). No new tasks need to launch; the ALB just flips back.
Two rollback triggers exist:  DEPLOYMENT_FAILURE  (hook reports failed, tasks won't start) and  DEPLOYMENT_STOP_ON_ALARM  (CloudWatch alarm fires during deployment). Both result in the same outcome — production traffic reverts to blue.

Approach 2 — ECS Native Blue/Green

CodeDeploy blue/green works — but as you saw, it requires a CodeDeploy application, deployment group, service role, AppSpec file, and Lambda hooks wired together through CodeDeploy's own lifecycle. What if ECS could handle all of this natively?

How It Differs from CodeDeploy

ECS native blue/green provides the same deployment safety without a separate CodeDeploy application. The key differences:
  • Deployment controller:  ECS  (not  CODE_DEPLOY )
  • No AppSpec file — deployment configuration is part of the ECS service definition
  • No CodeDeploy application/group/role — significantly less infrastructure to manage
  • Native lifecycle hooks — ECS invokes Lambda functions directly at 8 lifecycle stages (vs. CodeDeploy's 5)
  • Pause hooks — deployment can pause and wait for  ContinueServiceDeployment  API call (useful for manual approvals or external CI/CD integration)
  • Same result — test listener validation, canary/linear traffic shifting, alarm-based rollback
The ECS native lifecycle stages, in order:
  1.  RECONCILE_SERVICE  — service configuration reconciled
  1.  PRE_SCALE_UP  — before green task set scales up
  1.  POST_SCALE_UP  — green task set is healthy
  1.  TEST_TRAFFIC_SHIFT  — test listener shifts to green
  1.  POST_TEST_TRAFFIC_SHIFT  — after test traffic is on green (validate here)
  1.  PRE_PRODUCTION_TRAFFIC_SHIFT  — before production traffic moves (recurring for canary/linear)
  1.  PRODUCTION_TRAFFIC_SHIFT  — production traffic shifts (recurring for canary/linear)
  1.  POST_PRODUCTION_TRAFFIC_SHIFT  — after all production traffic is on green

Deployment Configuration — CodeDeploy Concepts Mapped to ECS Native

With CodeDeploy, deployment behavior is spread across multiple resources: the deployment group defines target groups, listeners, and termination wait time; the deployment config defines the traffic shifting strategy; the AppSpec wires in the hooks.
With ECS native, all of this collapses into a single deployment configuration on the ECS service itself. You specify:
  • Deployment type: blue/green (two task sets with ALB switching)
  • Production listener: which ALB listener serves real traffic
  • Test listener: which ALB listener routes to the replacement task set for validation
  • Target groups: the two target groups to switch between
  • Termination wait time: how long to keep the old task set alive after traffic shifts (rollback window)
  • Circuit breaker: auto-rollback on health check failures (replaces CodeDeploy's alarm + auto-rollback config)
CodeDeploy concept
ECS native equivalent
Deployment group (target groups, listeners)
Blue/green configuration on the service
Deployment config ( ECSCanary10Percent5Minutes )
Traffic shifting configuration (shown in canary/linear section below)
 TerminateBlueInstancesOnDeploymentSuccess 
Termination wait time
Alarm + auto-rollback config
Deployment circuit breaker + CloudWatch alarms
AppSpec hooks
ECS deployment lifecycle hooks (configured via API/CLI)

Prerequisites — CloudFormation Template (Native)

This template provisions the same base infrastructure (cluster, ALB, two target groups, two listeners) but without any CodeDeploy resources. The service starts with a rolling update strategy — we'll upgrade it to blue/green via CLI in the next step.
The full template is in the companion repo. Here are the key resources that differ from the CodeDeploy approach:
# ECS infrastructure role — allows ECS to manage load balancer resources for blue/green
ECSInfrastructureRole:
Type: AWS::IAM::Role
Properties:
RoleName: ecs-native-deploy-lab-infra-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers

# Service starts with default rolling update — upgraded to blue/green via CLI
Service:
Type: AWS::ECS::Service
DependsOn: ProductionListener
Properties:
ServiceName: native-deploy-lab-service
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 2
LaunchType: FARGATE
DeploymentController:
Type: ECS
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets: !Ref SubnetIds
SecurityGroups: [!Ref TaskSecurityGroup]
LoadBalancers:
- ContainerName: app
ContainerPort: 80
TargetGroupArn: !Ref BlueTargetGroup
The  ECSInfrastructureRole  is for ECS to manage ALB resources during blue/green deployments (replaces the CodeDeploy service role).
Deploy the stack:
aws cloudformation deploy \
--template-file ecs-native-prerequisites.yaml \
--stack-name ecs-native-lab \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
VpcId=<YOUR_VPC_ID> \
SubnetIds=<SUBNET_1>,<SUBNET_2>

# Store outputs
NATIVE_ALB=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`ALBDns`].OutputValue' --output text)
PROD_LISTENER=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`ProductionListenerArn`].OutputValue' --output text)
TEST_LISTENER=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`TestListenerArn`].OutputValue' --output text)
BLUE_TG=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`BlueTargetGroupArn`].OutputValue' --output text)
GREEN_TG=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`GreenTargetGroupArn`].OutputValue' --output text)
INFRA_ROLE=$(aws cloudformation describe-stacks --stack-name ecs-native-lab \
--query 'Stacks[0].Outputs[?OutputKey==`ECSInfrastructureRoleArn`].OutputValue' --output text)

# The update-service API requires Listener Rule ARNs, not Listener ARNs.
# Each listener has a default rule — retrieve those rule ARNs:
PROD_RULE=$(aws elbv2 describe-rules --listener-arn "$PROD_LISTENER" \
--query "Rules[0].RuleArn" --output text)
TEST_RULE=$(aws elbv2 describe-rules --listener-arn "$TEST_LISTENER" \
--query "Rules[0].RuleArn" --output text)

# Verify v1 is serving traffic
curl http://$NATIVE_ALB
# Expected: Hello World v1

Enabling Blue/Green on the Service

The service currently uses rolling update. Upgrade it to blue/green by updating the load balancer configuration (to wire in the alternate target group and listeners) and the deployment strategy. This requires two settings:
  1.  --load-balancers  with  advancedConfiguration  — tells ECS which alternate target group, listener rules, and IAM role to use for traffic shifting
  1.  --deployment-configuration  with  strategy: BLUE_GREEN  — enables the blue/green deployment strategy with a bake time
Important: The  productionListenerRule  and  testListenerRule  fields require Listener Rule ARNs, not Listener ARNs. Every listener has at least one default rule — that's what you pass here. The rule ARN has an additional  /rules/<rule-id>  segment compared to the listener ARN.
# Upgrade the service from rolling update to blue/green
aws ecs update-service \
--cluster ecs-native-deploy-lab \
--service native-deploy-lab-service \
--load-balancers '[{
"containerName": "app",
"containerPort": 80,
"targetGroupArn": "'"$BLUE_TG"'",
"advancedConfiguration": {
"alternateTargetGroupArn": "'"$GREEN_TG"'",
"productionListenerRule": "'"$PROD_RULE"'",
"testListenerRule": "'"$TEST_RULE"'",
"roleArn": "'"$INFRA_ROLE"'"
}
}]' \
--deployment-configuration '{
"strategy": "BLUE_GREEN",
"bakeTimeInMinutes": 5
}'
With the CodeDeploy approach, this same setup requires a separate CodeDeploy application, deployment group, service role, and AppSpec file. Here it's a single  update-service  call. The service is now ready for blue/green deployments.
Note: Enabling blue/green triggers an initial migration deployment — ECS creates a new task set and shifts traffic to it to establish the blue/green state. Wait for this deployment to complete (~2 minutes + bake time) before triggering the v2 deployment:
# Wait for the migration deployment to stabilize
aws ecs wait services-stable --cluster ecs-native-deploy-lab --services native-deploy-lab-service

Deploy v2 with ECS Native

With blue/green enabled, you trigger a deployment by updating the service's task definition ( update-service ):
  task-definition-v2.json   — same app as CodeDeploy but with family  ecs-native-deploy-lab . Grab it from the companion repo ( native/task-definition-v2.json ).
Register the task definition and update the service to trigger the deployment:
# Inject the execution role ARN
EXEC_ROLE=$(aws iam get-role --role-name ecs-native-deploy-lab-execution-role \
--query 'Role.Arn' --output text)

sed -i "s|<EXECUTION_ROLE_ARN>|$EXEC_ROLE|" task-definition-v2.json

# Register the new revision
NEW_TASK_DEF=$(aws ecs register-task-definition \
--cli-input-json file://task-definition-v2.json \
--query 'taskDefinition.taskDefinitionArn' --output text)

# Update the service — this triggers the blue/green deployment
aws ecs update-service \
--cluster ecs-native-deploy-lab \
--service native-deploy-lab-service \
--task-definition "$NEW_TASK_DEF"
ECS handles the rest: creates the green task set, waits for health checks, routes test traffic, and shifts production traffic. Poll both listeners to observe the blue/green progression:
# Poll both listeners every 10 seconds to observe the traffic shift
while true; do
PROD=$(curl -s http://$NATIVE_ALB:80)
TEST=$(curl -s http://$NATIVE_ALB:8080)
echo "$(date +%H:%M:%S) prod=$PROD test=$TEST"
if [ "$PROD" = "Hello World v2" ]; then break; fi
sleep 10
done
You'll see the progression:
09:30:25 prod=Hello World v1 test=Hello World v1 # green tasks starting up
09:30:35 prod=Hello World v1 test=Hello World v1 # still waiting for health checks
09:31:05 prod=Hello World v1 test=Hello World v2 # test listener shifted to green
09:31:15 prod=Hello World v1 test=Hello World v2 # bake time / validation window
09:31:25 prod=Hello World v2 test=Hello World v2 # production traffic shifted
The same blue/green mechanics as CodeDeploy — test listener routes to green first, production follows — but without the additional resources.

ECS Native Canary and Linear

The  update-service  command that enabled blue/green used an all-at-once traffic shift (the default). To use gradual traffic shifting, add a  trafficShiftingConfiguration  to the blue/green configuration.
Canary — shifts an initial percentage of traffic, waits, then shifts the rest:
"trafficShiftingConfiguration": {
"type": "CANARY",
"canaryConfiguration": {
"initialTrafficPercentage": 10,
"intervalInSeconds": 300,
"numberOfIntervals": 1
}
}
This shifts 10% of traffic for 5 minutes (300 seconds), then the remaining 90%.
Linear — shifts traffic in equal increments at regular intervals:
"trafficShiftingConfiguration": {
"type": "LINEAR",
"linearConfiguration": {
"trafficPercentageStep": 10,
"intervalInSeconds": 60
}
}
This shifts 10% every 60 seconds — full cutover in 10 minutes.
Both configurations go inside  blueGreenDeploymentConfiguration  in the  --deployment-configuration  argument. CloudWatch alarms can be attached at the ECS service level for automatic rollback during the observation windows — if an alarm fires mid-shift, ECS reverts traffic to the blue task set.

Comparison: Choosing Between Approaches

Aspect
CodeDeploy Blue/Green
ECS Native Blue/Green
ECS Rolling Update
Setup complexity
High (CodeDeploy app, group, role, AppSpec, hooks)
Medium (ECS service config)
Low (just min/max percent)
Lifecycle hooks
Yes — Lambda at 5 CodeDeploy stages
Yes — Lambda + pause hooks at 8 ECS stages
No
Test listener
Yes
Yes
No
Custom validation code
Yes ( AfterAllowTestTraffic )
Yes ( POST_TEST_TRAFFIC_SHIFT  Lambda hook)
No
Traffic strategies
AllAtOnce, Canary, Linear
AllAtOnce, Canary, Linear
Rolling (min healthy %)
Rollback speed
Instant (ALB switch)
Instant (ALB switch)
Slow (must launch old tasks again)
CodePipeline integration
CodeDeploy deploy action
ECS deploy action
ECS deploy action
When to use
Legacy pipelines, existing CodeDeploy infrastructure
New deployments (default choice)
Dev/test, simple services
Decision framework:
  • Starting fresh? → ECS Native. Same capabilities, less infrastructure, tighter integration.
  • Dev environment, minimal config? → Rolling Update

Clean Up

Check for orphaned resources before deleting stacks:
# Check for lingering task definitions (these persist even after stack deletion)
aws ecs list-task-definitions --family-prefix ecs-deploy-lab
aws ecs list-task-definitions --family-prefix ecs-native-deploy-lab

# Deregister all revisions (optional — they don't incur cost)
for arn in $(aws ecs list-task-definitions --family-prefix ecs-deploy-lab --query 'taskDefinitionArns[]' --output text); do
aws ecs deregister-task-definition --task-definition $arn > /dev/null
done
for arn in $(aws ecs list-task-definitions --family-prefix ecs-native-deploy-lab --query 'taskDefinitionArns[]' --output text); do
aws ecs deregister-task-definition --task-definition $arn > /dev/null;
done
Delete the CloudFormation stacks:
# Delete CodeDeploy approach stack
aws cloudformation delete-stack --stack-name ecs-codedeploy-lab
aws cloudformation wait stack-delete-complete --stack-name ecs-codedeploy-lab

# Delete ECS native approach stack
aws cloudformation delete-stack --stack-name ecs-native-lab
aws cloudformation wait stack-delete-complete --stack-name ecs-native-lab

# Delete the CloudWatch alarm if created
aws cloudwatch delete-alarms --alarm-names ecs-deploy-lab-5xx

Conclusion

ECS provides three deployment strategies — rolling update for simplicity, CodeDeploy blue/green for full lifecycle hook control with established pipelines, and ECS-native blue/green/canary/linear for the same safety with less configuration overhead.
The test listener pattern is the key safety mechanism for both blue/green approaches: validate the new version on port 8080 using real infrastructure before any production user on port 80 is affected. CodeDeploy uses  AfterAllowTestTraffic  hooks; ECS native uses  POST_TEST_TRAFFIC_SHIFT  Lambda hooks. Same concept, different wiring.
For new deployments, ECS native is the default choice — fewer resources to manage, native lifecycle hooks with 8 stages (vs. CodeDeploy's 5), and pause hooks for manual approval workflows. CodeDeploy remains relevant for teams with existing pipelines and deployment groups they don't want to migrate yet.
To go deeper:
Interested in choosing a deployment strategy for your application on ECS?  Let's talk !
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article