Documentation Index
Fetch the complete documentation index at: https://mintlify.com/terraform-aws-modules/terraform-aws-ecs/llms.txt
Use this file to discover all available pages before exploring further.
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
}
}
}