Skip to main content
Blue/green deployments replace running tasks with a new version while keeping the old version alive until traffic has been shifted and validated. This eliminates downtime during deploys and allows fast rollback if the new version fails health checks. This module supports two approaches:

ECS-native blue/green

Configured via deployment_configuration.strategy = "BLUE_GREEN". Managed entirely by ECS without CodeDeploy. Simpler to set up.

CodeDeploy blue/green

Uses ignore_task_definition_changes = true and an external aws_codedeploy_deployment_group. Required when CodeDeploy manages the deployment lifecycle.

ECS-native blue/green

The fargate example uses the ECS-native blue/green strategy with two ALB target groups:
module "ecs_service" {
  source = "terraform-aws-modules/ecs/aws//modules/service"

  name        = local.name
  cluster_arn = module.ecs_cluster.arn

  cpu    = 1024
  memory = 4096

  # Blue/green deployment
  deployment_configuration = {
    strategy             = "BLUE_GREEN"
    bake_time_in_minutes = 2
  }

  load_balancer = {
    service = {
      target_group_arn = module.alb.target_groups["ex-ecs"].arn
      container_name   = local.container_name
      container_port   = local.container_port

      # for blue/green deployments
      advanced_configuration = {
        alternate_target_group_arn = module.alb.target_groups["ex-ecs-alternate"].arn
        production_listener_rule   = module.alb.listener_rules["ex-http/production"].arn
        test_listener_rule         = module.alb.listener_rules["ex-http/test"].arn
      }
    }
  }

  subnet_ids = module.vpc.private_subnets
  security_group_ingress_rules = {
    alb_3000 = {
      description                  = "Service port"
      from_port                    = local.container_port
      ip_protocol                  = "tcp"
      referenced_security_group_id = module.alb.security_group_id
    }
  }
  security_group_egress_rules = {
    all = {
      ip_protocol = "-1"
      cidr_ipv4   = "0.0.0.0/0"
    }
  }

  tags = local.tags
}
You also need two ALB target groups and two listener rules (production and test):
module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "~> 10.0"

  # ... other config

  listeners = {
    ex-http = {
      port     = 80
      protocol = "HTTP"

      fixed_response = {
        content_type = "text/plain"
        message_body = "404: Page not found"
        status_code  = "404"
      }

      rules = {
        production = {
          priority = 1
          actions = [
            {
              weighted_forward = {
                target_groups = [
                  {
                    target_group_key = "ex-ecs"
                    weight           = 100
                  },
                  {
                    target_group_key = "ex-ecs-alternate"
                    weight           = 0
                  }
                ]
              }
            }
          ]
          conditions = [
            {
              path_pattern = {
                values = ["/*"]
              }
            }
          ]
        }
        test = {
          priority = 2
          actions = [
            {
              weighted_forward = {
                target_groups = [
                  {
                    target_group_key = "ex-ecs-alternate"
                    weight           = 100
                  }
                ]
              }
            }
          ]
          conditions = [
            {
              path_pattern = {
                values = ["/*"]
              }
            }
          ]
        }
      }
    }
  }

  target_groups = {
    ex-ecs = {
      backend_protocol                  = "HTTP"
      backend_port                      = local.container_port
      target_type                       = "ip"
      deregistration_delay              = 5
      load_balancing_cross_zone_enabled = true

      health_check = {
        enabled             = true
        healthy_threshold   = 5
        interval            = 30
        matcher             = "200"
        path                = "/"
        port                = "traffic-port"
        protocol            = "HTTP"
        timeout             = 5
        unhealthy_threshold = 2
      }

      create_attachment = false
    }

    ex-ecs-alternate = {
      backend_protocol                  = "HTTP"
      backend_port                      = local.container_port
      target_type                       = "ip"
      deregistration_delay              = 5
      load_balancing_cross_zone_enabled = true

      health_check = {
        enabled             = true
        healthy_threshold   = 5
        interval            = 30
        matcher             = "200"
        path                = "/"
        port                = "traffic-port"
        protocol            = "HTTP"
        timeout             = 5
        unhealthy_threshold = 2
      }

      create_attachment = false
    }
  }
}

CodeDeploy blue/green

For CodeDeploy-managed deployments, ECS hands off the deployment lifecycle to CodeDeploy. In this model, CodeDeploy (not Terraform) updates the service’s task definition and load balancer configuration during each deploy.

The role of ignore_task_definition_changes

Set ignore_task_definition_changes = true to instruct the module to ignore changes to the service’s task_definition and load_balancer arguments. This is required because:
  • CodeDeploy changes the task definition during each deployment to point to the new image.
  • CodeDeploy shifts the load balancer from the blue target group to the green target group.
Without this flag, Terraform would conflict with CodeDeploy by trying to revert those changes on the next terraform apply.
Changing ignore_task_definition_changes after the service has been created forces Terraform to destroy and recreate the ECS service. This causes downtime. Decide on this value before the first deployment and do not change it afterward.

Complete CodeDeploy example

module "ecs_service" {
  source = "terraform-aws-modules/ecs/aws//modules/service"

  # ... omitted for brevity

  ignore_task_definition_changes = true
}

resource "aws_lb_target_group" "this" {
  for_each = {
    blue  = {}
    green = {}
  }

  name = each.key

  # ... omitted for brevity
}

resource "aws_codedeploy_app" "this" {
  name             = "my-app"
  compute_platform = "ECS"
}

resource "aws_codedeploy_deployment_group" "this" {
  deployment_group_name = "my-deployment-group"
  app_name              = aws_codedeploy_app.this.name

  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

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

  # ... omitted for brevity

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = ["my-listener-arn"]
      }

      target_group {
        name = aws_lb_target_group.this["blue"].name
      }

      target_group {
        name = aws_lb_target_group.this["green"].name
      }
    }
  }
}

Traffic shifting strategies

The deployment_config_name in the aws_codedeploy_deployment_group controls how traffic shifts:
Config nameBehavior
CodeDeployDefault.ECSAllAtOnceShifts all traffic to the new version at once.
CodeDeployDefault.ECSLinear10PercentEvery1MinuteShifts 10% every minute until 100%.
CodeDeployDefault.ECSLinear10PercentEvery3MinutesShifts 10% every 3 minutes until 100%.
CodeDeployDefault.ECSCanary10Percent5MinutesShifts 10% initially, then 90% after 5 minutes.
CodeDeployDefault.ECSCanary10Percent15MinutesShifts 10% initially, then 90% after 15 minutes.
You can also define a custom deployment configuration with finer control over step percentages and bake times.

Canary deployments (EC2 capacity)

The ec2-autoscaling example demonstrates a canary strategy using the ECS-native deployment configuration:
module "ecs_service" {
  source = "terraform-aws-modules/ecs/aws//modules/service"

  # ... other config

  deployment_configuration = {
    strategy = "CANARY"

    canary_configuration = {
      canary_percent              = 10.0
      canary_bake_time_in_minutes = 5
    }
  }
}
This routes 10% of traffic to the new version for 5 minutes before shifting the remainder.

Build docs developers (and LLMs) love