Skip to main content
EC2 Auto Scaling capacity providers let you run ECS tasks on EC2 instances that you manage. This gives you control over instance types, AMIs, storage, and networking configuration — at the cost of additional operational overhead compared to Fargate.
You cannot mix EC2-based capacity providers with Fargate capacity providers on the same cluster. If you need Fargate, create a separate cluster.

Prerequisites

Before attaching an ASG as a capacity provider, you need an Auto Scaling Group. The terraform-aws-autoscaling module is the recommended way to create one. Key requirements for ASGs used with ECS:
  • The EC2 instances must run the ECS-optimized Amazon Linux 2023 AMI (or equivalent). Retrieve the latest AMI ID from SSM:
    data "aws_ssm_parameter" "ecs_optimized_ami" {
      name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended"
    }
    
  • Instances must have the AmazonEC2ContainerServiceforEC2Role IAM policy attached.
  • User data must register the instance with the correct ECS cluster:
    user_data = <<-EOT
      #!/bin/bash
    
      cat <<'EOF' >> /etc/ecs/ecs.config
      ECS_CLUSTER=${local.name}
      ECS_LOGLEVEL=debug
      ECS_CONTAINER_INSTANCE_TAGS=${jsonencode(local.tags)}
      ECS_ENABLE_TASK_IAM_ROLE=true
      EOF
    EOT
    
  • The ASG must have protect_from_scale_in = true when using managed_termination_protection = "ENABLED".
  • Tag the ASG with AmazonECSManaged = true to avoid provider issues:
    autoscaling_group_tags = {
      AmazonECSManaged = true
    }
    

Setting up the Auto Scaling Groups

The ec2-autoscaling example creates two ASGs — one for on-demand instances and one for Spot:
module "autoscaling" {
  source  = "terraform-aws-modules/autoscaling/aws"
  version = "~> 9.0"

  for_each = {
    # On-demand instances
    ex-1 = {
      instance_type              = "t3.large"
      use_mixed_instances_policy = false
      mixed_instances_policy     = null
      user_data                  = <<-EOT
        #!/bin/bash

        cat <<'EOF' >> /etc/ecs/ecs.config
        ECS_CLUSTER=${local.name}
        ECS_LOGLEVEL=debug
        ECS_CONTAINER_INSTANCE_TAGS=${jsonencode(local.tags)}
        ECS_ENABLE_TASK_IAM_ROLE=true
        EOF
      EOT
    }
    # Spot instances
    ex-2 = {
      instance_type              = "t3.medium"
      use_mixed_instances_policy = true
      mixed_instances_policy = {
        instances_distribution = {
          on_demand_base_capacity                  = 0
          on_demand_percentage_above_base_capacity = 0
          spot_allocation_strategy                 = "price-capacity-optimized"
        }

        launch_template = {
          override = [
            {
              instance_type     = "m4.large"
              weighted_capacity = "2"
            },
            {
              instance_type     = "t3.large"
              weighted_capacity = "1"
            },
          ]
        }
      }
      user_data = <<-EOT
        #!/bin/bash

        cat <<'EOF' >> /etc/ecs/ecs.config
        ECS_CLUSTER=${local.name}
        ECS_LOGLEVEL=debug
        ECS_CONTAINER_INSTANCE_TAGS=${jsonencode(local.tags)}
        ECS_ENABLE_TASK_IAM_ROLE=true
        ECS_ENABLE_SPOT_INSTANCE_DRAINING=true
        EOF
      EOT
    }
  }

  name = "${local.name}-${each.key}"

  image_id      = jsondecode(data.aws_ssm_parameter.ecs_optimized_ami.value)["image_id"]
  instance_type = each.value.instance_type

  security_groups                 = [module.autoscaling_sg.security_group_id]
  user_data                       = base64encode(each.value.user_data)
  ignore_desired_capacity_changes = true

  create_iam_instance_profile = true
  iam_role_name               = local.name
  iam_role_description        = "ECS role for ${local.name}"
  iam_role_policies = {
    AmazonEC2ContainerServiceforEC2Role = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
    AmazonSSMManagedInstanceCore        = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  }

  vpc_zone_identifier = module.vpc.private_subnets
  health_check_type   = "EC2"
  min_size            = 1
  max_size            = 5
  desired_capacity    = 2

  autoscaling_group_tags = {
    AmazonECSManaged = true
  }

  # Required for managed_termination_protection = "ENABLED"
  protect_from_scale_in = true

  use_mixed_instances_policy = each.value.use_mixed_instances_policy
  mixed_instances_policy     = each.value.mixed_instances_policy

  tags = local.tags
}

Attaching ASGs as capacity providers

Once the ASGs exist, reference their ARNs in the capacity_providers input of the cluster module. Each entry in the map creates one ECS capacity provider resource.
module "ecs_cluster" {
  source = "terraform-aws-modules/ecs/aws//modules/cluster"

  name = local.name

  default_capacity_provider_strategy = {
    ex-1 = {
      weight = 60
      base   = 20
    }
    ex-2 = {
      weight = 40
    }
  }

  capacity_providers = {
    # On-demand instances
    ex-1 = {
      auto_scaling_group_provider = {
        auto_scaling_group_arn         = module.autoscaling["ex-1"].autoscaling_group_arn
        managed_draining               = "ENABLED"
        managed_termination_protection = "ENABLED"

        managed_scaling = {
          maximum_scaling_step_size = 5
          minimum_scaling_step_size = 1
          status                    = "ENABLED"
          target_capacity           = 60
        }
      }
    }
    # Spot instances
    ex-2 = {
      auto_scaling_group_provider = {
        auto_scaling_group_arn         = module.autoscaling["ex-2"].autoscaling_group_arn
        managed_draining               = "ENABLED"
        managed_termination_protection = "ENABLED"

        managed_scaling = {
          maximum_scaling_step_size = 15
          minimum_scaling_step_size = 5
          status                    = "ENABLED"
          target_capacity           = 90
        }
      }
    }
  }

  tags = local.tags
}

Managed scaling configuration

Managed scaling allows ECS to automatically adjust the size of the ASG based on the number of tasks waiting to be placed.
FieldDescription
statusENABLED or DISABLED. Enables ECS-managed scaling of the ASG.
target_capacityTarget utilization percentage (1–100) for the capacity provider. ECS scales the ASG to reach this utilization.
minimum_scaling_step_sizeMinimum number of instances to add or remove in a single scaling action.
maximum_scaling_step_sizeMaximum number of instances to add or remove in a single scaling action.
instance_warmup_periodTime (in seconds) for a new instance to warm up before it contributes to scaling metrics.
Use a lower target_capacity (60–70%) for on-demand capacity providers to leave headroom for bursts. Use a higher value (90%) for Spot providers to maximize cost savings.

Managed draining and termination protection

Managed draining (managed_draining = "ENABLED") instructs ECS to automatically drain tasks from container instances before the ASG terminates them. This ensures graceful task shutdown during scale-in events. Managed termination protection (managed_termination_protection = "ENABLED") prevents the ASG from terminating instances that still have running ECS tasks. ECS removes the scale-in protection only after tasks have been safely moved.
When managed_termination_protection = "ENABLED", the ASG must have protect_from_scale_in = true set in the terraform-aws-autoscaling module. Without this, Terraform will return a validation error.
To disable managed draining (useful for testing or when using custom draining logic):
capacity_providers = {
  ex-1 = {
    auto_scaling_group_provider = {
      auto_scaling_group_arn         = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:..."
      managed_draining               = "DISABLED"
      managed_termination_protection = "ENABLED"

      managed_scaling = {
        maximum_scaling_step_size = 5
        minimum_scaling_step_size = 1
        status                    = "ENABLED"
        target_capacity           = 60
      }
    }
  }
}

Deploying a service onto EC2 capacity

Services that target EC2 capacity providers must set requires_compatibilities = ["EC2"] and reference the capacity provider by name:
module "ecs_service" {
  source = "terraform-aws-modules/ecs/aws//modules/service"

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

  # Task Definition
  requires_compatibilities = ["EC2"]
  capacity_provider_strategy = {
    # On-demand instances
    ex-1 = {
      capacity_provider = module.ecs_cluster.capacity_providers["ex-1"].name
      weight            = 1
      base              = 1
    }
  }

  container_definitions = {
    (local.container_name) = {
      image = "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest"
      portMappings = [
        {
          name          = local.container_name
          containerPort = local.container_port
          hostPort      = local.container_port
          protocol      = "tcp"
        }
      ]

      entrypoint = ["/usr/sbin/apache2", "-D", "FOREGROUND"]

      readonlyRootFilesystem = false

      enable_cloudwatch_logging              = true
      create_cloudwatch_log_group            = true
      cloudwatch_log_group_name              = "/aws/ecs/${local.name}/${local.container_name}"
      cloudwatch_log_group_retention_in_days = 7
    }
  }

  subnet_ids = module.vpc.private_subnets
  security_group_ingress_rules = {
    alb_http = {
      from_port                    = local.container_port
      description                  = "Service port"
      referenced_security_group_id = module.alb.security_group_id
    }
  }

  tags = local.tags
}

Attaching EBS volumes

EC2 tasks support attaching managed EBS volumes at launch time:
module "ecs_service" {
  source = "terraform-aws-modules/ecs/aws//modules/service"

  # ... other config

  volume_configuration = {
    name = "ebs-volume"
    managed_ebs_volume = {
      encrypted        = true
      file_system_type = "xfs"
      size_in_gb       = 5
      volume_type      = "gp3"
    }
  }

  volume = {
    my-vol = {}
    ebs-volume = {
      name                = "ebs-volume"
      configure_at_launch = true
    }
  }
}

Build docs developers (and LLMs) love