AWS ECS 使用 EC2 Capacity Provider (Managed Instances)

上篇 AWS ECS 使用 EC2 Capacity Provider (EC2 Auto Scaling) 学习了如何在 ECS 中使用 Capacity Provider + EC2 Auto Scaling 来部署一个简单的 Web 应用,以及了解 ECS 如何管理 EC2 的 Auto Scaling Group.

ECS Capacity Providers 是于 2019 年 12 月发布的,随同的功能支持了 Fargate, Fargate Spot, 和 EC2 AutoScaling. 而 Managed Instances 在 2025-09-30 才加入的新特性,见 Announcing Amazon ECS Managed Instances.

在近六年之间, AWS 大概也理解到了 Capacity Provider + EC2 Auto Scaling 的复杂性,虽说可由 ECS 来管理 EC2 的 ASG, 但毕竟有个 ASG 在那里。 ECS 与 EC2 ASG 之间联接由 CloudWatch Metrics, Alarms, EC2 ASG 的 Dynamic scaling policies 和 Lifecycle hooks 的一整套机制协作。 经常还不得不在 EC2 ASG 与 ECS 两个界面之间来回找问题。而 Managed Instances 的出现则将 EC2 实例的管理完全透明化了,在 EC2 端压根就不存在 一个相应的 ASG, 更不需要 CloudWatch Alarms 之类的关联组件。

Managed Instances 与 EC2 Auto Scaling 之间就好比 Serverless 与 非 Serverless 的区别,用 Managed Instances 之后你只管控制好 ECS Desired Count(ECS AutoScaling), 其余的都由 Managed Instances 来管理,在界面上只需要关注 ECS Infrastructure 中的 Container Instances. 由 Managed Instances 管理的 EC2 实例,你即使用有管理员权限都无法关闭它,只能全权由 Managed Instances 来控制。试图关闭这样的 EC2 实例时报错

相比于 ECS 直接用 EC2(ASG),通过 Capacity Provider 使用 EC2 的方式,还有一个好处就是可以实现 Min and max running tasks: 100% min and 200% max 的部署方式,即启动与当前等量的任务来替换全部旧任务,即使是高峰期也能较安心的部署。而 ECS 直接用 EC2 的时只能实现 Min and max running tasks: max 最大 100%, 所以不得不在 Rolling update, Blue/green, CanaryLinear 之间选择。

Managed Instances 可以配置 vCPU, Memory, GPU 等约束条件,在运行时由 Capacity Provider 自动为你选择 EC2 实例类型. 而对于计算型的任务 还是较倾向于直接锁定 EC2 的实例类型, 如 AMD CPU 的 c8a.4xlarge 等。

下面是一个 Web 应用部署实例,使用 t3.medium 实例类型,它有 2 vCPU, 4 GiB 内存, Web 任务需要 1 vCPU, 1 GiB 内存,这样实际在一个 t3.medium 实例上可以运行 2 个 Web 任务。

下面是相关的 Terraform 脚本如下

capacity-provider.tf

 1resource "aws_ecs_capacity_provider" "ec2" {
 2  name = "ec2-capacity-provider"
 3  cluster = aws_ecs_cluster.main.name
 4
 5  managed_instances_provider {
 6    infrastructure_role_arn = aws_iam_role.ecs_infrastructure.arn
 7    propagate_tags          = "CAPACITY_PROVIDER"
 8
 9    infrastructure_optimization {
10      scale_in_after = 300
11    }
12    instance_launch_template {
13      ec2_instance_profile_arn = var.ecs_instance_profile_arn
14      storage_configuration {
15        storage_size_gib = 30
16      }
17
18      network_configuration {
19        subnets         = var.subnet_ids
20        security_groups = var.ec2_security_groups
21      }
22
23      instance_requirements {
24        vcpu_count {
25          min = 2
26        }
27
28        memory_mib {
29          min = 4096
30        }
31        allowed_instance_types = ["t3.medium"]
32        burstable_performance = "included"
33      }
34    }
35  }
36  tags = {
37    Name = "test-cp"
38  }
39}
40
41data "aws_iam_policy_document" "ecs_infra_trust" {
42  statement {
43    actions = ["sts:AssumeRole"]
44    effect  = "Allow"
45    principals {
46      type        = "Service"
47      identifiers = ["ecs.amazonaws.com"]
48    }
49  }
50}
51
52resource "aws_iam_role" "ecs_infrastructure" {
53  name               = "ECSInfraRole"
54  assume_role_policy = data.aws_iam_policy_document.ecs_infra_trust.json
55}
56
57resource "aws_iam_role_policy_attachment" "ecs_infrastructure" {
58  role       = aws_iam_role.ecs_infrastructure.name
59  policy_arn = "arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances"
60}

通过 allowed_instance_types = ["t3.medium"] 来锁定使用 t3.medium 类型,注意 cpu 和 memory 的配置不能与此有冲突,并且 t3.mediumburstable 的类型, 所以必须加上 burstable_performance = "included" 才能选择到.

infrastructure_role_arn 需要指定为一个有 arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances policy, 能被 ecs.amazonaws.com assume 的 IAM role 即可。

使用 Managed Instances 时 EC2 除了可以指定 Security group, subnets, storage 外,其他都几乎都不能自定义。比如 AMI 是由 Managed Instances 指定的,像 ecs-managed-instances-standard-x86_64-20260220222634, 它是专门优化的,并且 EC2 会每 14 天被自动更新一次,来保证安全和性能的优化。

ecs.tf

 1resource "aws_ecs_cluster" "main" {
 2  name = "test-cp"
 3  setting {
 4    name  = "containerInsights"
 5    value = "enabled"
 6  }
 7}
 8
 9resource "aws_ecs_service" "main" {
10  name                 = "my-service"
11  cluster              = aws_ecs_cluster.main.id
12  task_definition      = aws_ecs_task_definition.main.arn
13  desired_count        = 1
14  network_configuration {
15    subnets         = var.subnet_ids
16    security_groups = var.ec2_security_groups
17  }
18
19  capacity_provider_strategy {
20    capacity_provider = aws_ecs_capacity_provider.ec2.name
21    base              = 1
22    weight            = 1
23  }
24
25  load_balancer {
26    target_group_arn = aws_lb_target_group.main.arn
27    container_name   = "my-container"
28    container_port   = 80
29  }
30}
31
32resource "aws_ecs_task_definition" "main" {
33  family                   = "my-task"
34  requires_compatibilities = ["EC2"]
35  network_mode             = "awsvpc"
36
37  container_definitions = jsonencode([
38    {
39      name        = "my-container"
40      image       = "strm/helloworld-http"
41      cpu         = 1024
42      memory      = 1024
43      stopTimeout = 5
44      portMappings = [
45        {
46          containerPort = 80
47          hostPort      = 80
48          protocol      = "tcp"
49        }
50      ]
51    }
52  ])
53}
54
55resource "aws_appautoscaling_target" "ecs_target" {
56  max_capacity       = 10
57  min_capacity       = 1
58  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
59  scalable_dimension = "ecs:service:DesiredCount"
60  service_namespace  = "ecs"
61}

Network 采用了 vpc, 每个容器将会从 VPC 获得自己的 IP 地址,Security Group 更易配置,初始启动一个任务。实际项目中应创建 ECS 的 AutoScaling 规则来控制 Desired Count, 后面测试将手工来调节。

elb.tf

 1resource "aws_lb" "test-cp" {
 2  name            = "test-cp-lb"
 3  internal        = true
 4  subnets         = var.subnet_ids
 5  security_groups = var.elb_security_groups
 6
 7}
 8
 9resource "aws_lb_listener" "main" {
10  port              = 80
11  protocol          = "HTTP"
12  load_balancer_arn = aws_lb.test-cp.arn
13  default_action {
14    type             = "forward"
15    target_group_arn = aws_lb_target_group.main.arn
16  }
17}
18
19resource "aws_lb_target_group" "main" {
20  name        = "test-cp-tg"
21  port        = 80
22  protocol = "HTTP"
23  vpc_id      = var.vpc_id
24  target_type = "ip"
25}

单纯测试 Managed Instances 类型的 Capacity Provider 可以不用建立 ELB, 作为一个较完备的应用还是加上这一层。

在运行以上的 Terraform 脚本时,请填入对应环境的以下变量

1  subnet_ids
2  ecs_instance_profile_arn
3  elb_security_groups
4  ec2_security_groups
5  vpc_id

准备好 AWS Credentials, 再补上 AWS Provider 后, 运行

1terraform init
2terraform apply-auto-approve

成功后会产生整个架构。在 ECS cluster test-cpInfrastructure 将会看到

使用了 Managed InstancesCapacity Provider 的管理界面就只要这个就行了。对于相应 EC2 的管理十分有限,不能指定 key_name, 也没有 Session Manager 可以连,不能自定义 userdata, 可以说 Managed Instances 产生的 EC2 是无法管理的。

Managed Instances 生成的 EC2 的 userdata 内容为

1[settings.ecs] 
2cluster = 'arn:aws:ecs:us-east-1:123456789000:cluster/test-cp' 
3awsvpc-block-imds = true 
4[settings.two] 
5platform-revision = 'Linux-X86_64-1.0.0-55' 
6available-memory = 3530 

下面是 Capacity Provider 的界面

下面直接改变 ECS Service my-service 的 Desired Count

  1. Desired Count 为 2: 因现有 t3.medium 实例上还有充足的 CPU/Memory 资源,所以直接在其上运行一个新任务
  2. Desired Count 为 3:由于现有实例 CPU 资源不足,所以必须新启一个 EC2 实例来运行新的任务
  3. Desired Count 为 4 时又会在新的 EC2 实例上运行一个新任务, 这里就不再往测试了
  4. 现在把 Desired Count 改为 2:发现从运行两个 Task 的 EC2 上停掉了一个任务,而不是从运行一个任务的 EC2 实例上停掉任务

再等了很久,也是用两个 EC2 实例各自运行一个 Task, 从资源来讲有些浪费, 看来还是优先考虑性能,也不会把一个任务从一个 EC2 实例上挪到另一个去。

  1. 把 Desired Count 改为 1,

那么什么时候会把空闲的 EC2 实例关闭呢?大概等了七八分钟,没有运行任务的 EC2 上出现 Deregistrating, 并且一两秒间从该列表中消失了。

剩下的事情就是 Capacity Provider 把该 EC2 实例也结束掉。

最后只省下一个 EC2 实例

而且这个是更老的 EC2 实例,Capacity Provider` 优先关闭新的 Task.

永久链接 https://yanbin.blog/aws-ecs-capacity-provider-managed-instances/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。