DEV Community

Cover image for ECS Blue/Green Deployments No Longer Need CodeDeploy

ECS Blue/Green Deployments No Longer Need CodeDeploy

For years, every blue/green deployment I ran on ECS had a second service bolted onto the side of it: AWS CodeDeploy. ECS ran the tasks. CodeDeploy owned the traffic shift, the bake window, and the rollback. Two services, two mental models, two sets of IAM roles, and an appspec.yaml that existed only to translate between them.

ECS now does all of it natively. You set the deployment strategy on the service, point it at two target groups, and ECS handles the traffic shift, the bake time, the CloudWatch-alarm rollback, and the lifecycle hooks itself. No CodeDeploy application. No deployment group. No appspec.yaml.

If you have been maintaining that CodeDeploy glue, you can delete it.

What Actually Changed

Native blue/green landed on ECS in July 2025. Linear and canary strategies followed in May 2026, so the feature set now covers the traffic-shifting patterns people used CodeDeploy for.

The old architecture split responsibility across two services:

  • ECS managed the task sets and registered them to target groups.
  • CodeDeploy watched the deployment, shifted the ALB listener between target groups, ran the bake period, checked alarms, and triggered rollback.

The wiring between them was the annoying part. You needed a CodeDeploy application, a deployment group, an appspec.yaml describing the container and port, a separate CodeDeploy IAM role, and a deployment controller set to CODE_DEPLOY on the service. Getting a new service into blue/green meant getting all of that right before your first deploy worked.

Native blue/green folds every one of those responsibilities into ECS. The traffic shift, bake period, managed rollback, and lifecycle hooks are ECS features now, configured in the service definition.

Side-by-side architecture

How It Works

ECS-native blue/green provisions a green service revision alongside the running blue revision and registers it to a separate target group behind your load balancer listener. Traffic shifts from blue to green using the strategy you pick, both revisions run together for a configurable bake period, and then ECS retires blue or rolls back if an alarm or hook fails.

The traffic shift itself uses Elastic Load Balancing weighted target groups. ECS adjusts the weights. For managed traffic shifting you need an Application Load Balancer, a Network Load Balancer, or Service Connect. A headless service (no load balancer) can still use blue/green for controlled rollouts, but ECS won't shift traffic for it because there's no traffic to shift.

Four strategies are available, and they differ only in how the weight moves:

Strategy Traffic move Rollback Cost during deploy Good for
Rolling Task-by-task, no shift Slow (redeploy) Low Cost-sensitive, simple services
Blue/green 0% to 100% in one switch Instant 2x tasks DB migrations, major upgrades
Linear Equal increments with bake between each Instant 2x tasks APIs, microservices
Canary Small slice first, then the rest Instant 2x tasks Changes needing careful validation, ML models

Rolling is the default and does no traffic shifting. The other three keep the blue revision alive during the bake window, which is what makes rollback instant: ECS shifts the weights back instead of redeploying tasks.

Four small traffic-shift

The Config

This is the part that shows the difference best. The whole deployment behavior lives in the service definition now.

You set the deployment controller to ECS (not CODE_DEPLOY) and choose a strategy in the deployment configuration:

{
  "deploymentController": { "type": "ECS" },
  "deploymentConfiguration": {
    "strategy": "BLUE_GREEN",
    "bakeTimeInMinutes": 10,
    "alarms": {
      "alarmNames": ["ecs-5xx-errors", "ecs-p99-latency"],
      "rollback": true,
      "enable": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For a canary rollout, you swap the strategy and add the canary shape:

{
  "deploymentConfiguration": {
    "strategy": "CANARY",
    "canaryConfiguration": {
      "canaryPercent": 5.0,
      "canaryBakeTimeInMinutes": 15
    },
    "alarms": {
      "alarmNames": ["ecs-5xx-errors", "checkout-error-rate"],
      "rollback": true,
      "enable": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Linear works the same way with a linearConfiguration instead: stepPercent (percentage shifted per step, default 10.0) and stepBakeTimeInMinutes (wait between steps, default 6).

The alarm block is what replaces CodeDeploy's rollback logic. When any listed CloudWatch alarm enters the ALARM state during the deployment, ECS rolls back. You point it at the metrics that actually indicate breakage, usually HTTPCode_Target_5XX_Count, TargetResponseTime, and any custom business metric you publish.

Lifecycle Hooks

CodeDeploy's appspec.yaml hooks (BeforeAllowTraffic, AfterAllowTraffic, and friends) were how you ran validation between traffic-shift steps. ECS has its own lifecycle hooks now, and they cover more of the deployment than CodeDeploy did.

A hook is a Lambda function wired to one or more stages. The Lambda returns SUCCEEDED to let the deployment continue, IN_PROGRESS while it's still checking, or FAILED to trigger a rollback.

The stages, in order:

Stage State when it runs
RECONCILE_SERVICE Only when a new deployment starts with more than one revision already active
PRE_SCALE_UP Green not started yet, blue serving 100%
POST_SCALE_UP Green started, blue still serving 100%, no test traffic
TEST_TRAFFIC_SHIFT Green ramping from 0% to 100% of test traffic
POST_TEST_TRAFFIC_SHIFT Green serving 100% of test traffic
PRE_PRODUCTION_TRAFFIC_SHIFT Before each production shift step
PRODUCTION_TRAFFIC_SHIFT Production traffic moving to green
POST_PRODUCTION_TRAFFIC_SHIFT Production shift complete

For linear and canary, the PRE_PRODUCTION_TRAFFIC_SHIFT and PRODUCTION_TRAFFIC_SHIFT stages fire at every increment, so a validation hook there runs on each step, not just once.

One constraint worth knowing up front: PAUSE hooks (the kind that just hold the deployment for manual approval) can't be attached to TEST_TRAFFIC_SHIFT or PRODUCTION_TRAFFIC_SHIFT. Those two stages only accept Lambda hooks. Pause where the deployment is at rest, validate with Lambda where traffic is moving.

A minimal validation hook looks like this:

def handler(event, context):
    stage = event["lifecycleStage"]
    green_arn = event["executionDetails"]["targetServiceRevisionArn"]

    if not smoke_test_passed(green_arn):
        return {"hookStatus": "FAILED"}

    return {"hookStatus": "SUCCEEDED"}
Enter fullscreen mode Exit fullscreen mode

ECS passes the traffic weights and the target revision ARN in the event, so the hook knows exactly which revision to test and how much traffic it's holding.

Horizontal deployment timeline

What You Have to Set Up

The load balancer side is where the real resources live. For an ALB-backed service you need:

  • Two target groups: one for blue, one for green
  • A production listener with a listener rule that ECS shifts weight across (you pass the rule ARN, not the listener ARN)
  • Optionally, a test listener so hooks can validate green before it takes production traffic
  • An IAM role that lets ECS modify the load balancer during a shift. The managed policy is AmazonECSInfrastructureRolePolicyForLoadBalancers (it grants elasticloadbalancing:ModifyListener, ModifyRule, RegisterTargets, DeregisterTargets, and the matching describe calls). ECS assumes this role to move the weights.
  • The service deployment controller set to ECS
  • The deployment strategy set to BLUE_GREEN, CANARY, or LINEAR
  • Bake time, alarms, and hooks as needed

The piece that trips people up is wiring the two target groups to the service. It lives in the load balancer block of the service definition, under advancedConfiguration:

{
  "loadBalancers": [
    {
      "containerName": "web",
      "containerPort": 80,
      "targetGroupArn": "<blue target group>",
      "advancedConfiguration": {
        "alternateTargetGroupArn": "<green target group>",
        "productionListenerRule": "<listener rule ARN>",
        "roleArn": "<the ELB-modify role above>"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

targetGroupArn is blue, alternateTargetGroupArn is green, and productionListenerRule is the rule ECS rewrites to shift traffic. Miss the roleArn or the rule ARN and service creation fails.

One thing that surprises people on the very first deploy: there's no existing "blue" yet, so ECS places the initial revision and sets the production rule weights itself. In practice the first task can land in the green target group at 100% weight while blue sits at 0. That's expected. ECS alternates blue and green across deployments, so which color is "live" flips each time.

If you're already running an ALB blue/green setup through CodeDeploy, you mostly have these pieces. The migration is deleting the CodeDeploy application, deployment group, appspec.yaml, and CodeDeploy IAM role, then moving the traffic-shift settings onto the ECS service definition.

In CDK the whole thing collapses into the FargateService construct:

service = ecs.FargateService(self, "Service",
    cluster=cluster,
    task_definition=task_definition,
    deployment_strategy=ecs.DeploymentStrategy.BLUE_GREEN,
)

service.add_lifecycle_hook(
    ecs.DeploymentLifecycleLambdaTarget(
        lambda_hook, "PreScaleHook",
        lifecycle_stages=[ecs.DeploymentLifecycleStage.PRE_SCALE_UP],
    )
)
Enter fullscreen mode Exit fullscreen mode

When Native Blue/Green Is the Right Call

Use native ECS blue/green when:

  • You're on ECS and want traffic-shifted deploys without running a second service
  • You're starting a new service and don't want to learn CodeDeploy's model
  • You want canary or linear rollouts with alarm-based rollback
  • Your validation logic fits Lambda hooks at defined stages
  • You're already maintaining CodeDeploy glue you'd rather delete

Stay on rolling deployments when:

  • You can't afford 2x task capacity during a deploy
  • The service tolerates task-by-task replacement fine
  • You don't need traffic-level control or instant rollback

Keep CodeDeploy only when:

  • You have deployment tooling built around CodeDeploy's APIs and events that isn't worth rewriting yet
  • You coordinate ECS deploys with other CodeDeploy-managed platforms (Lambda, EC2) in one pipeline and want a single control plane

For a greenfield ECS service, there's little reason to reach for CodeDeploy anymore. The native path is fewer resources and one service to reason about.

Things to Know Before You Migrate

You pay for 2x tasks during the deploy. Blue and green both run through the bake window. A long bake on a large service is real money for the duration. Size the bake time to how long your metrics take to surface a problem, not longer.

Rollback speed depends on the bake window. Instant rollback works because blue is still running. Once the bake period ends and blue is retired, "rollback" means a fresh deploy of the old revision. Don't set the bake time shorter than your slowest meaningful alarm.

Alarms must already exist and cover both target groups. ECS rolls back on alarm state, so an alarm that doesn't fire on the failure mode you care about gives you false confidence. Alarm on 5XX rate, latency percentiles, and at least one business metric that reflects the user experience.

Hooks that shift-path run on rollback too. Production and test traffic-shift hooks are invoked on the rollback path as well. If a hook is slow, it adds latency to your rollback. Keep shift-stage hooks fast.

PAUSE hooks are limited. You can't pause at the test or production traffic-shift stages. Put manual approval gates at the scale-up or post-shift stages instead.

Headless services don't get managed traffic shifting. No load balancer or Service Connect means no weighted shift. You can still use the deployment structure, but ECS won't move traffic for you.

Express Mode services can't do this. If you created your service through ECS Express Mode (the simplified experience that provisions the ALB, target groups, and scaling for you), you can't switch it to blue/green. Express manages deployments itself, and the console blocks the change with "Deployment controller cannot be updated for Express services." The deployment-strategy panel is read-only there. Everything in this post applies to standard services. If you want to pick and tune the strategy yourself, create a standard service, not an Express one.

The Takeaway

Blue/green on ECS used to be a two-service arrangement held together with an appspec.yaml. The deployment lived half in ECS and half in CodeDeploy, and the seam between them was where the setup pain and the debugging pain both lived.

Now the deployment lives in one place. You describe the strategy, the bake time, the alarms, and the hooks on the service, and ECS runs the whole thing. For a new service, skip CodeDeploy. For an existing one, the migration is mostly deletion.

Set the controller to ECS. Pick a strategy. Point it at two target groups. Ship.

Choosing between ECS Blue/Green Native or CodeDeploy | Gradual deployments with linear and canary | Blue/green implementation guide | Lambda lifecycle hooks

Top comments (0)