A multi-tenant SaaS runs on ECS Fargate behind an ALB — six services, anywhere from 2 to 24 tasks each depending on load. The team has been using ECS native rolling deploys since launch. After a deploy regression where a config bug poisoned half the tasks during the rolling cycle and the team had to drain the bad tasks manually, the architecture review proposes moving the customer-facing API to CodeDeploy blue/green. The question is: blue/green for everything, or only the customer-facing API. Both patterns work; the difference is in failure model, deploy time, and cost. Here is the decision matrix.
What’s happening
ECS native rolling deploy is the default UpdateService behavior. You update the task definition, ECS computes how many tasks to replace based on minimumHealthyPercent and maximumPercent, and it cycles tasks through registration with the target group. A typical config of minimumHealthyPercent: 100, maximumPercent: 200 means ECS launches a full new set of tasks, waits for them to be healthy, and only then deregisters the old ones. With minimumHealthyPercent: 50, maximumPercent: 100 the rolling is tighter — half are replaced at a time without launching extras.
CodeDeploy blue/green for ECS uses task sets instead. The service has two task sets — one labeled blue, one green. CodeDeploy creates the green task set with the new task definition, waits for the tasks to register healthy in a separate “green” target group, then swaps the listener rules from blue to green. The blue task set lingers for a configurable wait window before terminating.
The differences that matter in practice:
| Dimension | Rolling | Blue/green |
|---|---|---|
| Traffic shift mechanic | Per-task deregister/register | Listener-level target group swap |
| Rollback time | 5–10 minutes (re-deploy old task def) | Sub-minute (re-swap listener) |
| Compute cost during deploy | 100–200% (one extra set during overlap) | 200% (full second task set) |
| Wait window for late regressions | None (rollback is a fresh deploy) | Termination wait (10–60 min) |
| Listener config complexity | One target group | Two target groups + listener rules |
| Required IAM roles | ECS task execution role | ECS task execution role + CodeDeploy service role |
| Drain behavior | Per-task deregistration delay (default 300s) | Per-task on blue at termination |
Rolling is simpler — no extra target group, no service role, no separate deployment group. It is the right pattern when the service has a fast recovery path (a regression is caught by CloudWatch alarms within a few minutes and the team is comfortable re-deploying the previous task definition). It is the wrong pattern when a regression is latent — it surfaces 20 minutes after deploy on a slow endpoint that is not in the smoke test — because by then the rolling has completed and there is no fast way back.
Blue/green is the right pattern when:
- The service is customer-facing and a regression is measured in dollars per minute.
- The team has CloudWatch alarms that fire on a five-minute window — long enough to need a wait period to catch alarms before blue is gone.
- The cost of running a second task set for 15–30 minutes per deploy is acceptable. For a 24-task service on Fargate, this is two-to-four hours of extra task-time per deploy.
Rolling is the right pattern when:
- The service is internal or batch-oriented, and a regression is a minor incident rather than a customer-visible event.
- Deploy frequency is high — 10+ per day — and the cost of a second task set per deploy compounds.
- The task starts in under 30 seconds and the smoke test catches the obvious failures.
How to verify
Two checks decide the pattern: deploy frequency and recovery time. Pull both from existing data.
For deploy frequency, query CodeDeploy’s deployment history:
aws deploy list-deployments \
--application-name saas-api \
--include-only-statuses Succeeded Failed Stopped \
--create-time-range start=$(date -u -v-30d +%Y-%m-%dT%H:%M:%S),end=$(date -u +%Y-%m-%dT%H:%M:%S) \
--query 'length(deployments)'
For a service that deploys 5+ times per day, the recurring cost of double task sets adds up — that is a rolling candidate unless the customer impact of a regression is severe. For a service that deploys 2–3 times per week, blue/green is essentially free.
For recovery time, check the time-to-rollback on the last three incidents. If the recurring pattern is “deploy at 14:00, alarm at 14:18, rollback complete by 14:25”, rolling has been costing 25 minutes of degraded service per incident. Blue/green with a 20-minute termination wait would have rolled back the same incident in under 90 seconds.
A heuristic that holds in practice: cost of bad deploy minutes × deploys-per-month × probability-of-regression > extra compute cost of blue/green → use blue/green.
The fix
The full ECS service config for blue/green via CodeDeploy. The service is created with deploymentController.type: CODE_DEPLOY, two target groups in the same ALB, and two listener rules (one for production traffic, one for test traffic during the canary):
{
"serviceName": "saas-api",
"cluster": "saas-prod",
"taskDefinition": "saas-api:127",
"desiredCount": 12,
"launchType": "FARGATE",
"deploymentController": {"type": "CODE_DEPLOY"},
"loadBalancers": [{
"targetGroupArn": "arn:aws:elasticloadbalancing:ca-central-1:111122223333:targetgroup/saas-api-blue/abc",
"containerName": "api",
"containerPort": 8080
}],
"networkConfiguration": {
"awsvpcConfiguration": {
"subnets": ["subnet-aaa", "subnet-bbb"],
"securityGroups": ["sg-xxx"],
"assignPublicIp": "DISABLED"
}
}
}
The deployment group:
aws deploy create-deployment-group \
--application-name saas-api \
--deployment-group-name saas-api-prod \
--service-role-arn arn:aws:iam::111122223333:role/CodeDeployServiceRoleECS \
--deployment-config-name CodeDeployDefault.ECSAllAtOnce \
--deployment-style deploymentType=BLUE_GREEN,deploymentOption=WITH_TRAFFIC_CONTROL \
--blue-green-deployment-configuration '{
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 20
},
"deploymentReadyOption": {
"actionOnTimeout": "STOP_DEPLOYMENT",
"waitTimeInMinutes": 30
}
}' \
--ecs-services '[{"serviceName": "saas-api", "clusterName": "saas-prod"}]' \
--load-balancer-info '{
"targetGroupPairInfoList": [{
"targetGroups": [
{"name": "saas-api-blue"},
{"name": "saas-api-green"}
],
"prodTrafficRoute": {"listenerArns": ["arn:aws:elasticloadbalancing:ca-central-1:111122223333:listener/app/saas-alb/abc/443"]}
}]
}' \
--auto-rollback-configuration '{
"enabled": true,
"events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
}' \
--alarm-configuration '{
"enabled": true,
"alarms": [
{"name": "saas-api-5xx-rate"},
{"name": "saas-api-p95-latency"}
]
}'
The appspec.yaml for ECS is shorter than EC2 — no hooks (the equivalent run inside the task itself), just a reference to the new task definition:
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: arn:aws:ecs:ca-central-1:111122223333:task-definition/saas-api:128
LoadBalancerInfo:
ContainerName: api
ContainerPort: 8080
PlatformVersion: LATEST
Hooks:
- BeforeAllowTraffic: arn:aws:lambda:ca-central-1:111122223333:function:saas-api-before-traffic
- AfterAllowTraffic: arn:aws:lambda:ca-central-1:111122223333:function:saas-api-after-traffic
Three rules we hold when choosing between the two:
Match the deploy pattern to the customer impact. Customer-facing API on blue/green; internal admin panel on rolling; batch jobs on rolling. Not every service needs the same answer.
The cost difference is the second task set during the deploy window. On Fargate at 12 tasks of 1 vCPU / 2 GiB for 20 minutes per deploy, this is small but real — about 4 vCPU-hours per deploy. At 30 deploys/month, 120 vCPU-hours. Compare against the cost of a single bad-deploy incident; blue/green pays for itself after the first one it catches.
Do not mix patterns inside one CodePipeline. A pipeline that deploys to two services where one is rolling and one is blue/green will eventually deploy the wrong version pair when blue/green hangs in the termination wait and rolling completes. Either both services use the same pattern, or the pipeline stages them separately.
The rolling-vs-blue-green decision is not about which is better — it is about which failure model the service tolerates. Cross-reference the appspec anatomy reference for the hook ordering details across patterns.
Stack Harbor runs CodeDeploy + CodePipeline on production EC2 / ECS / Lambda fleets as part of Managed AWS and ongoing Managed Cloud Ops. If your deploys still take a maintenance window, start with an infrastructure consult.