如何在单EC2实例ECS集群中通过容量提供商与自动扩缩容实现任务重部署
使用Terraform创建单EC2实例的ECS集群,搭配容量提供商与自动扩缩容,目标是让单个任务运行在该实例上。初始部署正常,但更新任务定义替换现有任务时,新任务一直卡在PROVISIONING状态,直到把ASG的max_size从1改成2,新任务才会部署到新实例,旧实例随后因CloudWatch告警(15分钟内15个数据点的CapacityProviderReservation < 100)被移除。现在需要在非生产环境实现:集群仅保留1个实例,且允许在同一实例上多次部署任务。
相关Terraform代码
ECS服务配置
# ECS service resource "aws_ecs_service" "this" { name = "cluster" iam_role = aws_iam_role.ecs_role.arn cluster = aws_ecs_cluster.cluster.id task_definition = aws_ecs_task_definition.task_definition.arn desired_count = 1 force_new_deployment = true load_balancer { target_group_arn = aws_alb_target_group.lb.arn container_name = aws_ecs_task_definition.task_definition.family container_port = 80 } ordered_placement_strategy { type = "binpack" field = "memory" } capacity_provider_strategy { capacity_provider = aws_ecs_capacity_provider.ecs_capacity_provider.name base = 1 weight = 100 } lifecycle { create_before_destroy = true ignore_changes = [ desired_count ] } }
自动扩缩容组配置
# Auto scaling resource "aws_autoscaling_group" "ecs_asg" { name = "asg" vpc_zone_identifier = [for subnet in var.public_subnet_ids : subnet] max_size = 1 min_size = 1 desired_capacity = 1 health_check_type = "EC2" protect_from_scale_in = false launch_template { id = aws_launch_template.template.id version = "$Latest" } instance_refresh { strategy = "Rolling" } lifecycle { create_before_destroy = true } }
容量提供商配置
# capacity provider resource "aws_ecs_capacity_provider" "ecs_capacity_provider" { name = "ecs_capacity_provider" auto_scaling_group_provider { auto_scaling_group_arn = aws_autoscaling_group.ecs_asg.arn managed_termination_protection = "DISABLED" managed_scaling { maximum_scaling_step_size = 2 minimum_scaling_step_size = 1 status = "ENABLED" target_capacity = 100 } } } resource "aws_ecs_cluster_capacity_providers" "ecs_capacity_providers" { cluster_name = aws_ecs_cluster.cluster.name capacity_providers = [aws_ecs_capacity_provider.ecs_capacity_provider.name] default_capacity_provider_strategy { base = 1 weight = 100 capacity_provider = aws_ecs_capacity_provider.ecs_capacity_provider.name } }
ECS任务定义配置
# ECS Task resource "aws_ecs_task_definition" "task" { family = "task" container_definitions = jsonencode([ { name = "task", image = "test", cpu = "768", memory = "4096", essential = true portMappings = [ { containerPort = 80 hostPort = 80 protocol = "tcp" } ] logConfiguration = { logDriver = "awslogs", options = { "awslogs-group" = aws_cloudwatch_log_group.logs.name, "awslogs-region" = var.region, "awslogs-stream-prefix" = "app" } } } ]) execution_role_arn = aws_iam_role.ecs_exec.arn task_role_arn = aws_iam_role.ecs_task.arn }
触发的告警信息
第二次部署任务时,会触发自动扩缩容关联的CloudWatch告警:
告警名称:
TargetTracking-test-ecs-asg-AlarmHigh-e5a4556-5686-5669-26546-e745a5ed90cb
告警内容:目标追踪告警,当ECSServiceAverageCPUUtilization超过目标值(70%)时触发,触发条件为连续15个1分钟间隔的数据点超过阈值。
核心问题分析
问题根源在于部署策略+容量提供商的目标容量配置:
- ECS默认采用
先启动新任务、再停止旧任务的替换逻辑,但任务绑定了主机端口80,同一实例无法同时运行两个任务(端口冲突); - 容量提供商
target_capacity = 100的配置要求集群容量被100%利用,当部署新任务时,ECS判定当前实例无剩余资源,会触发ASG扩容,但max_size=1限制了扩容,导致新任务卡在PROVISIONING状态。
具体调整方案
1. 修改ECS服务部署策略,优先停止旧任务
在aws_ecs_service资源中添加部署控制配置,让ECS先停止旧任务再启动新任务,避免端口冲突:
resource "aws_ecs_service" "this" { # ... 原有配置 ... deployment_controller { type = "ECS" } deployment_configuration { maximum_percent = 100 # 同时运行的任务数不超过期望数量的100% minimum_healthy_percent = 0 # 允许健康任务数降到0,确保先停旧任务再启动新任务 } }
2. 调整容量提供商目标容量阈值
将managed_scaling中的target_capacity从100降低,给ECS预留容量判断空间,避免触发不必要的扩容:
resource "aws_ecs_capacity_provider" "ecs_capacity_provider" { # ... 原有配置 ... auto_scaling_group_provider { # ... 原有配置 ... managed_scaling { maximum_scaling_step_size = 2 minimum_scaling_step_size = 1 status = "ENABLED" target_capacity = 80 # 修改为80,避免容量全满时触发扩容逻辑 } } }
3. 可选:取消任务的主机端口绑定(推荐)
如果不需要固定主机端口,将portMappings中的hostPort设为0,让ECS自动分配随机端口,这样同一实例可同时运行多个任务,即使采用默认部署逻辑也不会冲突:
container_definitions = jsonencode([ { # ... 原有配置 ... portMappings = [ { containerPort = 80 hostPort = 0 # 改为0,自动分配主机端口 protocol = "tcp" } ] # ... 原有配置 ... } ])
验证调整效果
应用Terraform变更后,再次更新任务定义:
- 新任务会在同一实例上先停旧任务、再启动新任务;
- 不会触发ASG扩容,集群始终保持1个EC2实例;
- CloudWatch扩容告警不会被误触发。
内容的提问来源于stack exchange,提问作者Thiago Scodeler

