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.
| Field | Description |
|---|
status | ENABLED or DISABLED. Enables ECS-managed scaling of the ASG. |
target_capacity | Target utilization percentage (1–100) for the capacity provider. ECS scales the ASG to reach this utilization. |
minimum_scaling_step_size | Minimum number of instances to add or remove in a single scaling action. |
maximum_scaling_step_size | Maximum number of instances to add or remove in a single scaling action. |
instance_warmup_period | Time (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
}
}
}