Back to Blog
· 9 min read · EN

Blue-Green Deployment on ECS: Zero Downtime Release Strategy

A complete guide to implementing blue-green deployments on ECS Fargate using CodeDeploy, with instant rollback, traffic shifting, and Terraform configuration.

DevOpsTutorial #ecs#fargate#aws#deployment#blue-green#codedeploy#zero-downtime#terraform
Blue-Green Deployment on ECS: Zero Downtime Release Strategy

Rolling deployments work for most services. You update tasks one at a time, ECS drains old connections, done. But when you need instant rollback capability or cannot tolerate mixed API versions serving traffic simultaneously, blue-green is the right approach.

I use blue-green deployments for user-facing APIs, payment services, and any service where a bad deployment needs to be reversed in seconds, not minutes. The setup is more complex than rolling, but the operational benefits are significant.

This builds on the CI/CD pipeline with GitHub Actions and ECS guide. If you have not set up basic ECS deployments yet, start there.

How Blue-Green Works on AWS

The AWS implementation uses CodeDeploy as the traffic controller:

ALB (HTTPS:443)

    ├── Production listener rule → Blue target group (current live)
    └── Test listener rule → Green target group (new version)

During deployment:
1. CodeDeploy registers new tasks in Green target group
2. Green tasks pass health checks
3. CodeDeploy shifts production traffic from Blue to Green
4. Blue tasks remain alive during wait period
5. After wait period, Blue tasks terminate

Rollback:
1. Shift production traffic back to Blue (< 30 seconds)
2. Terminate Green tasks

Two target groups on one ALB. One is live, one is staging the new version. Traffic switches atomically. No requests hit both versions simultaneously.

Prerequisites

You need:

  • An existing ECS cluster and service
  • An ALB with HTTPS listener
  • CodeDeploy application and deployment group
  • Two target groups on the ALB

All managed with Terraform below.

Terraform: Full Infrastructure Setup

ALB with Two Target Groups

# Blue target group (initial active)
resource "aws_lb_target_group" "blue" {
  name        = "my-api-blue"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    enabled             = true
    path                = "/health"
    port                = "8080"
    protocol            = "HTTP"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 10
    matcher             = "200"
  }

  deregistration_delay = 30
}

# Green target group (receives new deployments)
resource "aws_lb_target_group" "green" {
  name        = "my-api-green"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    enabled             = true
    path                = "/health"
    port                = "8080"
    protocol            = "HTTP"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 10
    matcher             = "200"
  }

  deregistration_delay = 30
}

# Production listener: routes traffic to Blue by default
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.blue.arn
  }

  lifecycle {
    ignore_changes = [default_action]  # CodeDeploy manages this
  }
}

# Test listener: used by CodeDeploy to validate Green before traffic switch
resource "aws_lb_listener" "test" {
  load_balancer_arn = aws_lb.main.arn
  port              = 8080
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.green.arn
  }

  lifecycle {
    ignore_changes = [default_action]
  }
}

The ignore_changes lifecycle block is important. CodeDeploy updates the listener target groups during deployments. Without this, the next terraform apply would revert the listener to point at Blue even after a successful deployment to Green.

ECS Service with CODE_DEPLOY Controller

resource "aws_ecs_service" "api" {
  name            = "my-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 3

  launch_type = "FARGATE"

  # Use CODE_DEPLOY instead of default ECS controller
  deployment_controller {
    type = "CODE_DEPLOY"
  }

  network_configuration {
    subnets          = aws_subnet.private[*].id
    security_groups  = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  # Load balancer connects to Blue target group initially
  load_balancer {
    target_group_arn = aws_lb_target_group.blue.arn
    container_name   = "api"
    container_port   = 8080
  }

  lifecycle {
    ignore_changes = [
      task_definition,    # CodeDeploy manages this
      load_balancer,      # CodeDeploy manages this
    ]
  }
}

CodeDeploy Application and Deployment Group

resource "aws_codedeploy_app" "api" {
  compute_platform = "ECS"
  name             = "my-api"
}

resource "aws_iam_role" "codedeploy" {
  name = "codedeploy-ecs-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "codedeploy.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "codedeploy" {
  role       = aws_iam_role.codedeploy.name
  policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}

resource "aws_codedeploy_deployment_group" "api" {
  app_name               = aws_codedeploy_app.api.name
  deployment_group_name  = "my-api-deployment-group"
  service_role_arn       = aws_iam_role.codedeploy.arn
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

  ecs_service {
    cluster_name = aws_ecs_cluster.main.name
    service_name = aws_ecs_service.api.name
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout    = "CONTINUE_DEPLOYMENT"
      wait_time_in_minutes = 0  # Switch immediately after health checks pass
    }

    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 5  # Keep Blue alive for 5 minutes after switch
    }
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.https.arn]
      }

      test_traffic_route {
        listener_arns = [aws_lb_listener.test.arn]
      }

      target_group {
        name = aws_lb_target_group.blue.name
      }

      target_group {
        name = aws_lb_target_group.green.name
      }
    }
  }
}

The termination_wait_time_in_minutes = 5 keeps the old Blue environment alive for 5 minutes after traffic switches to Green. During this window, you can manually trigger a rollback if you observe issues after the switch. After 5 minutes, Blue terminates automatically.

Traffic Shifting Strategies

CodeDeploy provides three built-in strategies:

All-At-Once

Traffic: 100% Blue → 100% Green (instantaneous)

Configuration:

deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

Best for: internal services, non-critical APIs, fast iteration. Zero validation window but fastest deployment.

Canary

Traffic: 100% Blue → 10% Green (5 min validation) → 100% Green

Configuration:

deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"

Or create a custom config:

resource "aws_codedeploy_deployment_config" "canary_10" {
  deployment_config_name = "my-api-canary-10"
  compute_platform       = "ECS"

  traffic_routing_config {
    type = "TimeBasedCanary"

    time_based_canary {
      interval   = 5   # Wait 5 minutes at 10%
      percentage = 10  # Start with 10% traffic to Green
    }
  }
}

Best for: user-facing APIs where you want to validate with real traffic before full rollout. If the 10 percent window shows errors, CodeDeploy rolls back automatically before the full switch.

Linear

Traffic: 100% Blue → 10% Green → 20% Green → ... → 100% Green (10% every 5 min)

Configuration:

resource "aws_codedeploy_deployment_config" "linear_10" {
  deployment_config_name = "my-api-linear-10"
  compute_platform       = "ECS"

  traffic_routing_config {
    type = "TimeBasedLinear"

    time_based_linear {
      interval   = 5   # Increment every 5 minutes
      percentage = 10  # Add 10% each increment
    }
  }
}

Best for: high-traffic services where gradual rollout reduces risk, or when you want to monitor error rates at each traffic increment before proceeding.

GitHub Actions Integration

Update your GitHub Actions workflow to trigger a CodeDeploy deployment instead of directly updating the ECS service:

- name: Create AppSpec file
  run: |
    cat > appspec.json << EOF
    {
      "version": 0.0,
      "Resources": [{
        "TargetService": {
          "Type": "AWS::ECS::Service",
          "Properties": {
            "TaskDefinition": "${{ steps.task-def.outputs.task-definition-arn }}",
            "LoadBalancerInfo": {
              "ContainerName": "api",
              "ContainerPort": 8080
            }
          }
        }
      }]
    }
    EOF

- name: Create deployment
  id: deploy
  run: |
    DEPLOYMENT_ID=$(aws deploy create-deployment \
      --application-name my-api \
      --deployment-group-name my-api-deployment-group \
      --revision revisionType=AppSpecContent,appSpecContent={content="$(cat appspec.json | jq -c .)"} \
      --query 'deploymentId' \
      --output text)
    echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT

- name: Wait for deployment
  run: |
    aws deploy wait deployment-successful \
      --deployment-id ${{ steps.deploy.outputs.deployment_id }}

The aws deploy wait deployment-successful command blocks until the deployment completes or fails. If CodeDeploy detects health check failures or triggers auto-rollback, the command exits with a non-zero code and fails the workflow.

Monitoring Deployments

CloudWatch Alarms for Auto-Rollback

Configure CodeDeploy to roll back automatically if error rates spike during deployment:

resource "aws_cloudwatch_metric_alarm" "high_error_rate" {
  alarm_name          = "my-api-high-error-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "HTTPCode_Target_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = 60
  statistic           = "Sum"
  threshold           = 10
  treat_missing_data  = "notBreaching"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
  }
}

# Attach alarm to CodeDeploy deployment group
resource "aws_codedeploy_deployment_group" "api" {
  # ... other config ...

  alarm_configuration {
    alarms  = [aws_cloudwatch_metric_alarm.high_error_rate.name]
    enabled = true
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }
}

If the high-error-rate alarm fires during a deployment, CodeDeploy rolls back automatically. Traffic switches back to Blue, Green terminates, workflow fails with a meaningful error.

Deployment Notifications

resource "aws_sns_topic" "deploy_notifications" {
  name = "ecs-deploy-notifications"
}

resource "aws_codedeploy_deployment_group" "api" {
  # ... other config ...

  trigger_configuration {
    trigger_events = [
      "DeploymentStart",
      "DeploymentSuccess",
      "DeploymentFailure",
      "DeploymentRollback",
    ]
    trigger_name       = "deploy-notification"
    trigger_target_arn = aws_sns_topic.deploy_notifications.arn
  }
}

This sends SNS notifications for every deployment event. Connect to Slack via Lambda or use AWS Chatbot for direct Slack integration.

Manual Rollback

If you need to roll back manually during the Blue environment wait window:

# Get the active deployment ID
DEPLOYMENT_ID=$(aws deploy list-deployments \
  --application-name my-api \
  --deployment-group-name my-api-deployment-group \
  --include-only-statuses Succeeded \
  --query 'deployments[0]' \
  --output text)

# Stop and roll back
aws deploy stop-deployment \
  --deployment-id $DEPLOYMENT_ID \
  --auto-rollback-enabled

This switches traffic back to Blue and terminates Green tasks. Execution time: 30 seconds or less.

After the wait window expires and Blue terminates, manual rollback is no longer possible via CodeDeploy. In that case, trigger a new deployment with the previous task definition revision.

Rolling vs Blue-Green: Decision Guide

FactorRollingBlue-Green
Mixed versions during deployYesNo
Rollback speed5-10 minutesUnder 30 seconds
Cost during deploymentSame2x compute
ComplexityLowMedium
Supports canary trafficNoYes
Works with stateful connectionsHardYes

Use rolling deployment when:

  • Your service is stateless and API is backwards-compatible
  • Rollback speed of 5 to 10 minutes is acceptable
  • You want the simplest possible setup

Use blue-green deployment when:

  • Your API is user-facing and cannot serve mixed versions
  • SLA requires rollback in under 1 minute
  • You want canary traffic validation before full cutover
  • Your service has long-lived connections that need graceful handling

For most internal microservices, rolling deployment with a circuit breaker is sufficient. I use blue-green for payment APIs, customer-facing endpoints, and services where a bad deploy directly impacts user experience.

If you need help setting up blue-green deployments for your ECS services, I offer DevOps support that covers CodeDeploy setup, ALB configuration, and GitHub Actions integration. This pattern is also covered in my CI/CD pipeline guide where the blue-green option is discussed alongside rolling deployments.

For portfolio examples of production deployment pipelines, see my insurance platform DevOps work and mini DevOps projects.