FargateからECS Managed Instanceへの移行手順を確認してみた ~Terraformを添えて~

FargateからECS Managed Instanceへの移行手順を確認してみた ~Terraformを添えて~

2025.10.28

中山です

表題の通り、FargateからECS Managed Instanceへの移行手順を確認してみました。

ECS Managed Instanceとは

ECS Managed Instanceは、Amazon ECSで利用できる新しい種類のコンピューティングリソースです。

新しいとはいっても、その実体はEC2インスタンスであり、その運用をAWSが担うというものです。

EC2の柔軟性を持ちつつ、Fargateのような運用の手軽さがある、といった感じのようです。

やってみた

というわけで、実際に使ってみようと思います。

今回は、Fargateを利用してる環境を再現した上で、それをECS Managed Instanceに移行してみます。

また、TerraformがECS Managed Instanceをすでにサポートしていたので、Terraformで環境の構築および変更を実施します。

事前準備

VPCの作成

VPCに関してはマネジメントコンソールのウィザードで サボりました 作成しました。

ポイントは以下の通り。詳細はスクリーンショットをご覧ください。

  • Public / Private Subnetの2層
  • 3AZ
  • NAT Gatewayあり

ecs_namaged_instance_20251024_01

移行前環境の準備

移行前の環境をTerraformで定義します。

作成しているリソースは、ざっくり以下の通りです(細かいものは一部割愛)。

  • ECS cluster
  • Task execution role
  • Task role
  • Task definition
  • ALB
  • Target group
  • ALB listener
  • ECS service

ECS Taksにはnginxのコンテナイメージを利用しています。

移行前
main.tf
provider "aws" {
  region = "ap-northeast-1"
}

data "aws_region" "current" {}

resource "aws_ecs_cluster" "test" {
  name = "test-cluster"
}

data "aws_iam_policy_document" "ecs_task_execution_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "ecsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "ecsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role_policy.json
}

resource "aws_cloudwatch_log_group" "test" {
  name              = "test-logs"
  retention_in_days = 7
}

resource "aws_ecs_task_definition" "test" {
  family = "test-task-definition"

  requires_compatibilities = ["FARGATE"]

  runtime_platform {
    cpu_architecture        = "X86_64"
    operating_system_family = "LINUX"
  }

  cpu                = "256"
  memory             = "512"
  network_mode       = "awsvpc"
  execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn      = aws_iam_role.ecs_task_role.arn

  container_definitions = jsonencode([
    {
      name  = "test-task"
      image = "public.ecr.aws/nginx/nginx:latest"
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
          protocol      = "tcp"
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.test.name
          awslogs-region        = data.aws_region.current.region
          awslogs-stream-prefix = "test"
        }
      }
      essential = true
    }
  ])
}

resource "aws_security_group" "test_alb" {
  name        = "test-alb-security-group"
  description = "Security group for ALB"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_http_inbound" {
  security_group_id = aws_security_group.test_alb.id

  from_port   = 80
  to_port     = 80
  ip_protocol = "tcp"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_vpc_security_group_egress_rule" "allow_healthcheck_outbound" {
  security_group_id = aws_security_group.test_alb.id

  ip_protocol = "-1"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_lb" "test" {
  name               = "test-alb"
  load_balancer_type = "application"
  subnets            = var.vpc_public_subnet_ids
  security_groups    = [aws_security_group.test_alb.id]
  internal           = false
  ip_address_type    = "ipv4"
}

resource "aws_lb_target_group" "test" {
  name     = "test-target-group"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  target_type = "ip"

  health_check {
    path = "/"
  }

  deregistration_delay = 30  
}

resource "aws_lb_listener" "metropolis" {
  load_balancer_arn = aws_lb.test.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.test.arn
  }
}

resource "aws_security_group" "test_task" {
  name        = "test-task-security-group"
  description = "Security group for ECS tasks"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_alb_inbound" {
  security_group_id = aws_security_group.test_task.id

  from_port                    = 80
  to_port                      = 80
  ip_protocol                  = "tcp"
  referenced_security_group_id = aws_security_group.test_alb.id
}

resource "aws_vpc_security_group_egress_rule" "allow__outbound" {
  security_group_id = aws_security_group.test_task.id

  ip_protocol = "-1"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_ecs_service" "test" {
  name = "test-service"

  cluster         = aws_ecs_cluster.test.id
  task_definition = aws_ecs_task_definition.test.arn

  desired_count = 1

  launch_type      = "FARGATE"
  platform_version = "LATEST"

  load_balancer {
    target_group_arn = aws_lb_target_group.test.arn
    container_name   = "test-task"
    container_port   = 80
  }

  network_configuration {
    subnets          = var.vpc_private_subnet_ids
    security_groups  = [aws_security_group.test_task.id]
    assign_public_ip = false
  }

  availability_zone_rebalancing = "ENABLED"

  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100

  health_check_grace_period_seconds = 30
}
variables.tf
variable "vpc_id" {
  type        = string
  description = "VPC ID where the ECS instances will be launched"
}

variable "vpc_public_subnet_ids" {
  type        = list(string)
  description = "Public subnet IDs for the VPC"
}

variable "vpc_private_subnet_ids" {
  type        = list(string)
  description = "Private subnet IDs for the VPC"
}

移行の流れ

移行の流れはこちらのドキュメントに記載があります。

今回は以下の流れで移行を行いました。

  • Infrastructure roleおよびInstance profileの作成
  • Capacity Provider の作成
  • タスク定義の修正
  • ECS Serviceの修正

Infrastructure roleおよびInstance profileの作成

Managed Instanceを利用するにあたり、2つのIAM Roleを作成する必要があります。

Infrastructure roleは、Amazon ECSがインスタンスの起動等を実行するために必要なRoleです。

ユースケースに応じて必要な権限は異なりますが、単純にコンテナを起動するだけであれば以下のManaged Policyを利用すればよいです(ユースケースに応じて追加のポリシーをアタッチする必要があります)。

併せて、Instance profileを作成します。
こちらは、Managed Instanceにアタッチするインスタンスプロファイルです。

こちらについては以下のManaged Policyをアタッチするとよいでしょう。

今回作成したTerraformのコードは以下の通りです。

data "aws_iam_policy_document" "ecs_infrastructure_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_infrastructure" {
  name               = "ecsInfrastructureRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_infrastructure_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_infrastructure_role_policy_attachment" {
  role       = aws_iam_role.ecs_infrastructure.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances"
}

data "aws_iam_policy_document" "ecs_instance_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_instance" {
  name               = "ecsInstanceRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_instance_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_instance_role_policy_attachment" {
  role       = aws_iam_role.ecs_instance.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECSInstanceRolePolicyForManagedInstances"
}

resource "aws_iam_instance_profile" "ecs_instance" {
  name = "ecsInstanceRole"
  role = aws_iam_role.ecs_instance.name
}

AmazonECSInfrastructureRolePolicyForManagedInstancesの内容で気になったところ

ポリシーの中身を見ていたのですが、PassRoleする権限を定義したStatementが少し気になりました。

PassRoleできるロール名の接頭辞が ecsInstanceRole に限定されているので、IAM Roleの名前を雑に設定すると起動に失敗します。

抜粋
    {
      "Sid" : "PassInstanceRoleForManagedInstances",
      "Effect" : "Allow",
      "Action" : [
        "iam:PassRole"
      ],
      "Resource" : [
        "arn:aws:iam::*:role/ecsInstanceRole*"
      ],
      "Condition" : {
        "StringEquals" : {
          "iam:PassedToService" : "ec2.amazonaws.com"
        }
      }
    },

実際に試した結果、以下のエラーでタスクの開始(インスタンスの起動)に失敗しました。

Stopped reason

ResourceInitializationError: Unable to launch instance(s) for capacity provider test-capacity-provider. UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/ecsInfrastructureRole/ECSManagedInstances is not authorized to perform: iam:PassRole on resource: arn:aws:iam::xxxxxxxxxxxx:role/test-ecsInstanceRole because no identity-based policy allows the iam:PassRole action. RequestId: 09c9ee6d-5143-40a1-b36e-c45ba80d923b

Capacity Provider の作成

Capacity Providerを作成します。

Capacity Providerを利用してどのようなコンピューティングリソースを利用したいか定義します。

TerraformでCapacity Providerを利用する方法はこちらを参照してください。

今回作成したTerraformのコードは以下の通りです。

resource "aws_ecs_capacity_provider" "test" {
  name    = "test-capacity-provider"
  cluster = aws_ecs_cluster.test.name

  managed_instances_provider {
    infrastructure_role_arn = aws_iam_role.ecs_infrastructure.arn

    propagate_tags = "NONE"

    instance_launch_template {
      ec2_instance_profile_arn = aws_iam_instance_profile.ecs_instance.arn
      monitoring               = "BASIC"

      instance_requirements {
        memory_mib {
          min = 2048
          max = 8192
        }

        vcpu_count {
          min = 1
          max = 4
        }

        instance_generations      = ["current"]
        cpu_manufacturers         = ["intel", "amd"]
        require_hibernate_support = true
      }

      network_configuration {
        subnets         = var.vpc_private_subnet_ids
        security_groups = [aws_security_group.test_task.id]
      }

      storage_configuration {
        storage_size_gib = 30
      }
    }
  }
}

先ほど作成したInfrastructure roleおよびInstance profileおよびネットワーク設定の他に、managed_instances_provider - instance_launch_template - instance_requirements でどのようなインスタンスを利用したいか指定することができます。

気になったところ

capacity providerではECS Managed Instance attributes (custom)としてmax_spot_price_as_percentage_of_optimal_on_demand_priceおよびspot_max_price_percentage_over_lowest_priceを指定できる(Applyも成功する)ようなのですが、現時点では特に意味をなさないようでした。

スポットインスタンスを利用するための設定が見当たりませんでしたが、今後サポートされるのでしょうか?
公式ドキュメントにも記載が見当たらない(RI/SPとインスタンスに複数タスクを集約することにしか言及してない)ので、ちょっと様子を見ようと思います。

タスク定義の修正

次にタスク定義を修正します。

タスク定義では requires_compatibilities で利用可能なコンピューティングリソースの種類を設定できます。

今回のサンプルでは FARGATE のみを指定しているので、ここに MANAGED_INSTANCES を追加します(消してもいいのですが、一旦追加とします)。

resource "aws_ecs_task_definition" "test" {
  family = "test-task-definition"

- requires_compatibilities = ["FARGATE"]
+ requires_compatibilities = ["FARGATE", "MANAGED_INSTANCES"]

  runtime_platform {
    cpu_architecture        = "X86_64"
    operating_system_family = "LINUX"
  }

  cpu                = "256"
  memory             = "512"
  network_mode       = "awsvpc"
  execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn      = aws_iam_role.ecs_task_role.arn

  container_definitions = jsonencode([
    {
      name  = "test-task"
      image = "public.ecr.aws/nginx/nginx:latest"
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
          protocol      = "tcp"
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.test.name
          awslogs-region        = data.aws_region.current.region
          awslogs-stream-prefix = "test"
        }
      }
      essential = true
    }
  ])
}

ECS Serviceの修正

最後にECS Serviceを修正します。

従来は、launch_type でFargateを指定していましたが、これを先ほど作成したCapacity Providerの設定に移行します。

設定のポイントは以下の通りです。

  • platform_versionlaunch_type にFargateを指定する時にのみ適用されるため、併せて削除します。
  • capacity_provider_strategy を追加する際、force_new_deploymenttrue にする必要があります(設定しなかったらエラーになった)。
resource "aws_ecs_service" "test" {
  name = "test-service"

  cluster         = aws_ecs_cluster.test.id
  task_definition = aws_ecs_task_definition.test.arn

  desired_count = 1

- launch_type      = "FARGATE"
- platform_version = "LATEST"

  load_balancer {
    target_group_arn = aws_lb_target_group.test.arn
    container_name   = "test-task"
    container_port   = 80
  }

  network_configuration {
    subnets          = var.vpc_private_subnet_ids
    security_groups  = [aws_security_group.test_task.id]
    assign_public_ip = false
  }

  availability_zone_rebalancing = "ENABLED"

  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100

  health_check_grace_period_seconds = 300

+ capacity_provider_strategy {
+   capacity_provider = aws_ecs_capacity_provider.test.name
+   weight            = 1
+ }

+ force_new_deployment = true
}

動作確認

Terraformのコードの準備ができましたので、Fargateのタスクが起動している状態から変更を実施していきます。

今回は、「Infrastructure roleおよびInstance profileの作成」+「Capacity Provider の作成」と「タスク定義の修正」+「ECS Serviceの修正」の2段階でApplyしました。
まとめてApplyすると、Capacity Providerが作成される前にECS Serviceの更新が開始されてエラーになったためです。

修正前のタスクの状態はこちらで、Fargate上でタスクが起動していることが確認できます。

ecs_namaged_instance_20251024_02

この状態から、2回に分けて修正内容をApplyします。
結果、タスクがManaged Instanceに移行できました。

ecs_namaged_instance_20251024_03

最終的なコードがこちらです。

移行後
main.tf
provider "aws" {
  region = "ap-northeast-1"
}

data "aws_region" "current" {}

resource "aws_ecs_cluster" "test" {
  name = "test-cluster"
}

data "aws_iam_policy_document" "ecs_task_execution_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "ecsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_task_role" {
  name               = "ecsTaskRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role_policy.json
}

resource "aws_cloudwatch_log_group" "test" {
  name              = "test-logs"
  retention_in_days = 7
}

resource "aws_ecs_task_definition" "test" {
  family = "test-task-definition"

  requires_compatibilities = ["FARGATE", "MANAGED_INSTANCES"]

  runtime_platform {
    cpu_architecture        = "X86_64"
    operating_system_family = "LINUX"
  }

  cpu                = "256"
  memory             = "512"
  network_mode       = "awsvpc"
  execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
  task_role_arn      = aws_iam_role.ecs_task_role.arn

  container_definitions = jsonencode([
    {
      name  = "test-task"
      image = "public.ecr.aws/nginx/nginx:latest"
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
          protocol      = "tcp"
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.test.name
          awslogs-region        = data.aws_region.current.region
          awslogs-stream-prefix = "test"
        }
      }
      essential = true
    }
  ])
}

resource "aws_security_group" "test_alb" {
  name        = "test-alb-security-group"
  description = "Security group for ALB"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_http_inbound" {
  security_group_id = aws_security_group.test_alb.id

  from_port   = 80
  to_port     = 80
  ip_protocol = "tcp"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_vpc_security_group_egress_rule" "allow_healthcheck_outbound" {
  security_group_id = aws_security_group.test_alb.id

  ip_protocol = "-1"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_lb" "test" {
  name               = "test-alb"
  load_balancer_type = "application"
  subnets            = var.vpc_public_subnet_ids
  security_groups    = [aws_security_group.test_alb.id]
  internal           = false
  ip_address_type    = "ipv4"
}

resource "aws_lb_target_group" "test" {
  name     = "test-target-group"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  target_type = "ip"

  health_check {
    path = "/"
  }

  deregistration_delay = 30  
}

resource "aws_lb_listener" "metropolis" {
  load_balancer_arn = aws_lb.test.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.test.arn
  }
}

resource "aws_security_group" "test_task" {
  name        = "test-task-security-group"
  description = "Security group for ECS tasks"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "allow_alb_inbound" {
  security_group_id = aws_security_group.test_task.id

  from_port                    = 80
  to_port                      = 80
  ip_protocol                  = "tcp"
  referenced_security_group_id = aws_security_group.test_alb.id
}

resource "aws_vpc_security_group_egress_rule" "allow__outbound" {
  security_group_id = aws_security_group.test_task.id

  ip_protocol = "-1"
  cidr_ipv4   = "0.0.0.0/0"
}

resource "aws_ecs_service" "test" {
  name = "test-service"

  cluster         = aws_ecs_cluster.test.id
  task_definition = aws_ecs_task_definition.test.arn

  desired_count = 1

  load_balancer {
    target_group_arn = aws_lb_target_group.test.arn
    container_name   = "test-task"
    container_port   = 80
  }

  network_configuration {
    subnets          = var.vpc_private_subnet_ids
    security_groups  = [aws_security_group.test_task.id]
    assign_public_ip = false
  }

  availability_zone_rebalancing = "ENABLED"

  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100

  health_check_grace_period_seconds = 30

  capacity_provider_strategy {
    capacity_provider = aws_ecs_capacity_provider.test.name

    weight = 1
    base   = 0
  }

  force_new_deployment = true
}

data "aws_iam_policy_document" "ecs_infrastructure_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_infrastructure" {
  name               = "ecsInfrastructureRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_infrastructure_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_infrastructure_role_policy_attachment" {
  role       = aws_iam_role.ecs_infrastructure.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances"
}

data "aws_iam_policy_document" "ecs_instance_assume_role_policy" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_instance" {
  name               = "ecsInstanceRole"
  assume_role_policy = data.aws_iam_policy_document.ecs_instance_assume_role_policy.json
}

resource "aws_iam_role_policy_attachment" "ecs_instance_role_policy_attachment" {
  role       = aws_iam_role.ecs_instance.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECSInstanceRolePolicyForManagedInstances"
}

resource "aws_iam_instance_profile" "ecs_instance" {
  name = "ecsInstanceRole"
  role = aws_iam_role.ecs_instance.name
}

resource "aws_ecs_capacity_provider" "test" {
  name    = "test-capacity-provider"
  cluster = aws_ecs_cluster.test.name

  managed_instances_provider {
    infrastructure_role_arn = aws_iam_role.ecs_infrastructure.arn

    propagate_tags = "NONE"

    instance_launch_template {
      ec2_instance_profile_arn = aws_iam_instance_profile.ecs_instance.arn
      monitoring               = "BASIC"

      instance_requirements {
        memory_mib {
          min = 2048
          max = 8192
        }

        vcpu_count {
          min = 1
          max = 4
        }

        instance_generations      = ["current"]
        cpu_manufacturers         = ["intel", "amd"]
        require_hibernate_support = true
      }

      network_configuration {
        subnets         = var.vpc_private_subnet_ids
        security_groups = [aws_security_group.test_task.id]
      }

      storage_configuration {
        storage_size_gib = 30
      }
    }
  }
}

感想

Fargateからの移行シナリオを通してECS Managed Instanceを使ってみました。

使用感はFargateにかなり近い印象でした。

ECS on Fargateを利用している方は、Fargateにおける機能上の制約やスケールしたときのコストが気になっているという方もいるのではないかと思います。
そういった方にはECS Managed Instanceを検討してみるといいのではないかと思います。

ECS on EC2を利用している方についても、on EC2における構成や運用が複雑で認知コストが高かったり運用作業の負担の大きさが気になっている方もいらっしゃると思います。
そういった場合にも検討する価値があると思います。

気になったこと

併せて、実際に触ってみていくつか気になったことがありました。

  • タスク数が少ない時にAZ分散できてない場合がある
  • Managed Instanceの余剰リソース
  • インスタンスの再起動時の挙動およびメンテナンスウィンドウ

タスク数が少ない時にAZ分散できてない場合がある

他の方のブログ記事でも言及があったのですが、タスクの増やし方次第ではタスクが特定のAZに偏ることを私も確認しました。

具体的には、以下の様な状況です。

  • ECS Taskが1つだけ起動している
  • それに伴ってContainer Instanceも1つだけ起動している

この状況下でタスクを増やすと、起動済のContainer Instanceでタスクが起動しました。

Amazon ECS Managed Instanceは AWS Fargate と何が違うのか?

ちなみに、タスク数が0の状態でタスク数(=Desired count)を3(=利用しているAZ数)にしたところ、全てのAZにContainer Instanceが起動し、タスクも各AZに分散されました。

可用性が求められる状況であれば、タスク数を利用しているAZの倍数でDesired countを操作すればいいのかなーと思いました。

スケーリングに関するロジックはもう少し詳細なものが公開されるとありがたいなーと思います。

Managed Instanceの余剰リソース

今回の検証では、タスクのサイズとしてCPU=256Unit / Memory=512MBを設定していました。

その前提でタスク数を増加させていくと、Container InstanceのCPUが3/4余る状況が発生しました。

ecs_namaged_instance_20251024_04

今回起動したコンテナインスタンスのインスタンスタイプはc6a.mediumで、CPUとメモリの比率はタスクのサイズ比と同じでした。

きっちり使い切りそうな印象ですが、実際にはメモリの一部がタスク以外に消費されており、結果としてCPUの余剰が発生していました(OSやAgentで利用する分を考慮していると推測)。
タスクを停止してContainer Instanceにおける利用可能なCPU/メモリの量を確認したところ。466MBほどがタスクが起動していない状態でも消費されていることがわかります(この値がインスタンスタイプによって変わるのか、固定なのかは未確認)。

ecs_namaged_instance_20251024_05

実際に計算してみましたが、Fargateで4タスク起動したときの料金とManaged Instanceの料金はほぼ同じで、Fargateで3タスク起動したときの料金と比べるとかなり割高になってしまうようです。

コストの最適化を意図してECS Managed Instanceの利用を検討する場合、以下の様な対応が必要でしょう。

  • Managed Instanceのリソースを効率的に利用できるようタスクのサイズを調整したり、利用するインスタンスタイプをCapacity Providerで制御する
  • Compute Savings Plansより割引率の高いEC2 Savings PlansやReserved Instanceの活用を検討する

公式ドキュメントでもその旨は記載がありましたので、適宜参照してください。

ちなみに、タスクのサイズを512MB→395MBに変更したところ、以下の様にリソースをきっちり使えました(実際はここまで厳格にやる方が効率悪いはずなのでほどほどにしたほうがいいと思います)。

ecs_namaged_instance_20251024_06

インスタンスの再起動時の挙動およびメンテナンスウィンドウ

Managed Instanceのパッチ管理等はAWSがその責任を負います。それに伴い、14~21日のサイクルでインスタンスが再起動します。

By default, Amazon ECS managed container instances operate on a standardized 14–21 day lifecycle. Amazon ECS initiates graceful workload draining at day 14 from instance launch, with final termination occurring no later than day 21.

その再起動がどのようなタイミングで行われるのか気になるところですが、EC2のカスタムイベントウィンドウで制御することができます。

カスタムイベントウィンドウの対象インスタンスの特定にはタグを利用できます。Capacity Providerの propagate_tags プロパティでインスタンスへのタグの付与を自動化できますので適宜利用するとよいでしょう。

また、タスクの入れ替えは「代替タスクの起動→既存タスクの停止」の順序で行われる仕様のようです(実際に入れ替わる様子は未確認)。

Amazon ECS employs a start-before-stop strategy for service tasks (or as per the Amazon ECS service configuration), ensuring replacement tasks are launched before stopping existing ones, minimizing service disruption. Throughout this process, Amazon ECS services honor all service deployment configurations while continuing draining attempts until day 21 from the instance launch.

その他

コンテナをPullする際のキャッシュやセキュリティモデルもFargateとは異なるので必要に応じて確認するとよいでしょう。

まとめ

当初思ったよりもFargateに近い使用感でしたが、コストや可用性には十分な注意が必要そうだなーと思いました。

なにより、Managed Instanceに何を求めるのか?を明確にした上で利用を検討いただくとよいでしょう。

現場からは以上です。

この記事をシェアする

FacebookHatena blogX

関連記事