2025年9月頃に発表されたECSのマネージドインスタンスを触ってみたのでブログに残します。
ECSマネージドインスタンスとは
ECSの起動タイプの1つでAWS側に管理されたEC2インスタンス上でタスクを動かすことができる機能です。
今までECSではFargateとEC2上でタスクを動かすことができました。
FargateはサーバーレスでEC2を使用せずにAWSが全て管理するような起動タイプでEC2と異なりインスタンスタイプの選択などが不要でシンプルに使えていました。
ただし、GPUが使用できなかったりと制限もある起動タイプとなっています。
EC2起動タイプではFargateとは異なりインスタンスタイプを選択できたりとFargateよりも自由度が高いですが、EC2の管理 (OSのアップデートやパッチ適用など) やスケーリングの管理が必要で運用が少し大変な起動タイプです。
ECSマネージドインスタンスではEC2上でタスクが動いていますが、このEC2はAWS側に管理されるためパッチの適用やスケーリングの管理をAWS側に任せることが可能となっています。
このEC2はBottlerocketと呼ばれるコンテナを動かすために作られたOSが動いているようです。(SSHなどは提供されていないようです)
パッチの適用は14日間に1度行われるため、そのたびにEC2が入れ替わるようです。
なので、アプリケーションではステートレスな作りが必要になってきます。
Ensure security compliance and regular patching with a maximum instance lifetime of 14 days, after which tasks are automatically migrated to new instances.
今回作成する構成
雑ですが、以下の構成を作成していきます。
Fargateかマネージドインスタンスかの違いくらいでここら辺は殆ど同じ構成でいけます。

設定
少し調べたところ基本的には前回作成したブログのCloudFormationを少し修正すれば起動できそうなので使える部分は流用します。
リソースは以下のCloudFormationテンプレートで作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
- Label:
default: Parameters for Network
Parameters:
- VPCCIDR
- PublicSubnet01CIDR
- PublicSubnet02CIDR
- PrivateSubnet01CIDR
- PrivateSubnet02CIDR
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
VPCCIDR:
Default: 192.168.0.0/16
Type: String
PublicSubnet01CIDR:
Default: 192.168.0.0/24
Type: String
PublicSubnet02CIDR:
Default: 192.168.1.0/24
Type: String
PrivateSubnet01CIDR:
Default: 192.168.2.0/24
Type: String
PrivateSubnet02CIDR:
Default: 192.168.3.0/24
Type: String
Resources:
# ------------------------------------------------------------#
# CloudWatch Logs
# ------------------------------------------------------------#
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/ecs-${env}-log"
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
ECSInstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInstanceRolePolicyForManagedInstances
RoleName: !Sub ecsInstanceRole-iam-${env}-ecs-instance-role
EC2IAMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub ecsInstanceRole-${env}-instanceprofile-ec2
Roles:
- !Ref ECSInstanceRole
ECSInstanceInfrastructureRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances
RoleName: !Sub iam-${env}-ecs-instance-infrastructure-role
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
RoleName: !Sub iam-${env}-ecs-task-execution-role
ECSLBRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers
RoleName: !Sub iam-${env}-ecs-lb-role
ECSLambdaInvokeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub iam-${env}-ecs-lambda-invoke-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: 'lambda:InvokeFunction'
Resource: '*'
RoleName: !Sub iam-${env}-ecs-lambda-invoke-role
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub vpc-${env}
# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub igw-${env}
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------#
PublicSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PublicSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub1
VpcId: !Ref VPC
PublicSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PublicSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub2
VpcId: !Ref VPC
PrivateSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PrivateSubnet01CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv1
VpcId: !Ref VPC
PrivateSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PrivateSubnet02CIDR
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv2
VpcId: !Ref VPC
# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------#
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-pub
PublicRouteTableRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet01
PublicRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet02
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-prv
PrivateRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet01
PrivateRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet02
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
ALBSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for alb
GroupName: !Sub securitygroup-${env}-alb
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 80
- FromPort: 8080
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 8080
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-alb
VpcId: !Ref VPC
ECSSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for ecs
GroupName: !Sub securitygroup-${env}-ecs
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
SourceSecurityGroupId: !Ref ALBSG
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-ecs
VpcId: !Ref VPC
VPCEndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for vpc endpoint
GroupName: !Sub securitygroup-${env}-vpc-endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 443
IpProtocol: tcp
SourceSecurityGroupId: !Ref ECSSG
ToPort: 443
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-vpc-endpoint
VpcId: !Ref VPC
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
ECSAgentEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs-agent
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECSTelemetryEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs-telemetry
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECSEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECRdkrEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECRapiEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
LogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: false
Name: !Sub alb-${env}-ecs
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSG
Subnets:
- !Ref PublicSubnet01
- !Ref PublicSubnet02
Tags:
- Key: Name
Value: !Sub alb-${env}-ecs
Type: application
TargetGroup1:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-01
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-01
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
TargetGroup2:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-02
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-02
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
ALBHTTPListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
StatusCode: 403
Type: fixed-response
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
ALBHTTPListenerRule1:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup1
Type: forward
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- "/*"
ListenerArn: !Ref ALBHTTPListener1
Priority: 1
ALBHTTPListener2:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
StatusCode: 403
Type: fixed-response
LoadBalancerArn: !Ref ALB
Port: 8080
Protocol: HTTP
ALBHTTPListenerRule2:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup2
Type: forward
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- "/*"
ListenerArn: !Ref ALBHTTPListener2
Priority: 1
# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------#
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ecs-${env}-cluster
ECSCapacityProvider:
Type: AWS::ECS::CapacityProvider
Properties:
ClusterName: !Ref ECSCluster
ManagedInstancesProvider:
InfrastructureOptimization:
ScaleInAfter: 1800
InfrastructureRoleArn: !GetAtt ECSInstanceInfrastructureRole.Arn
InstanceLaunchTemplate:
Ec2InstanceProfileArn: !GetAtt EC2IAMInstanceProfile.Arn
InstanceRequirements:
BurstablePerformance: included
CpuManufacturers:
- intel
- amd
MemoryMiB:
Max: 5120
Min: 1024
VCpuCount:
Max: 5
Min: 1
Monitoring: DETAILED
NetworkConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
StorageConfiguration:
StorageSizeGiB: 30
PropagateTags: CAPACITY_PROVIDER
Name: !Sub ${env}-ecs-managed-instance-provider
ECSCapacityProviderAssociations:
Type: AWS::ECS::ClusterCapacityProviderAssociations
Properties:
CapacityProviders:
- !Ref ECSCapacityProvider
Cluster: !Ref ECSCluster
DefaultCapacityProviderStrategy:
- CapacityProvider: !Ref ECSCapacityProvider
Weight: 1
ECSTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-${env}:latest
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref "AWS::Region"
awslogs-stream-prefix: !Sub ecs-${env}-log
Name: !Sub task-${env}
PortMappings:
- ContainerPort: 80
Cpu: 256
ExecutionRoleArn: !Ref TaskExecutionRole
Family: !Sub task-${env}
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- MANAGED_INSTANCES
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener1
- ALBHTTPListener2
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LoadBalancers:
- AdvancedConfiguration:
AlternateTargetGroupArn: !Ref TargetGroup2
ProductionListenerRule: !Ref ALBHTTPListenerRule1
RoleArn: !GetAtt ECSLBRole.Arn
TestListenerRule: !Ref ALBHTTPListenerRule2
ContainerName: !Sub task-${env}
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
ServiceName: !Sub service-${env}
TaskDefinition: !Ref ECSTaskDef
DeploymentController:
Type: ECS
DeploymentConfiguration:
Strategy: BLUE_GREEN
LifecycleHooks:
- HookTargetArn: !GetAtt Lambda.Arn
LifecycleStages:
- POST_TEST_TRAFFIC_SHIFT
RoleArn: !GetAtt ECSLambdaInvokeRole.Arn
# ------------------------------------------------------------#
# AutoScaling
# ------------------------------------------------------------#
ServiceAutoScalingRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub iam-${env}-ecs-autoscaling-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: application-autoscaling.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: !Sub iam-${env}-ecs-autoscaling-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- application-autoscaling:*
- cloudwatch:DescribeAlarms
- cloudwatch:PutMetricAlarm
- ecs:DescribeServices
- ecs:UpdateService
Resource: "*"
ServiceScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MinCapacity: 1
MaxCapacity: 4
ResourceId: !Sub service/${ECSCluster}/${ECSService.Name}
RoleARN: !GetAtt ServiceAutoScalingRole.Arn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
ServiceScaleOutPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: ecs-autoscaling-policy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref ServiceScalingTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70.0
ScaleInCooldown: 300
ScaleOutCooldown: 300
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
LambdaIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub policy-${env}-lambda
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
Lambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import json
import urllib3
import logging
import base64
import os
import time
# ログ記録を設定します
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# HTTP クライアントを初期化します
http = urllib3.PoolManager()
def lambda_handler(event, context):
logger.info(f"Event: {json.dumps(event)}")
logger.info(f"Context: {context}")
try:
# 実際のシナリオでは、テストエンドポイント URL を作成します
test_endpoint = os.getenv("APP_URL")
# Getリクエストを実行
response = http.request('GET', "http://" + test_endpoint + ":8080")
logger.info(f"GET / response status: {response.status}")
# レスポンスに OK ステータスコード (200~299 の範囲) が含まれているかどうかを確認します
time.sleep(60)
if 200 <= response.status < 300:
logger.info("Get request success - received OK status code")
return {
"hookStatus": "SUCCEEDED"
}
else:
logger.error(f"Get request failed - status code: {response.status}")
return {
"hookStatus": "FAILED"
}
except Exception as error:
logger.error(f"Get request failed failed: {str(error)}")
return {
"hookStatus": "FAILED"
}
Environment:
Variables:
APP_URL: !GetAtt ALB.DNSName
FunctionName: !Sub lambda-${env}-ecs-hook
Handler: index.lambda_handler
Role: !GetAtt LambdaIAMRole.Arn
Runtime: python3.13
Timeout: 90
ネットワーク部分やALBの部分の説明は省きますが、IAMとECS周りは以下のようになっています。
IAM
IAM周りのリソースは65~156行目で作成しています。
前回作成していたタスク実行ロールやBlue/Green用のロールだけでなく、ECSからEC2を作成したりするのに使用するインフラストラクチャロールを作成しています。
マネージドポリシーとしてAmazonECSInfrastructureRolePolicyForManagedInstancesを設定しています。
このポリシーを設定することで起動テンプレートなどがECSから作成できるようになっています。
また、マネージドインスタンスに設定するIAMロールを作成しており、こちらのIAMロールではEC2からECSへアクセスするためのポリシーAmazonECSInstanceRolePolicyForManagedInstancesを設定しています。
注意点としてはAmazonECSInfrastructureRolePolicyForManagedInstancesの中にあるiam:PassRoleが"arn:aws:iam::*:role/ecsInstanceRole*"のようにIAMロールを制限しているので、マネージドインスタンスに設定するIAMロール名はecsInstanceRoleから始まるように設定する必要があります。
カスタマイズしたい場合はIAMポリシーを1から作成する形になります。
- Amazon ECS infrastructure IAM role - Amazon Elastic Container Service
- Amazon ECS Managed Instances instance profile - Amazon Elastic Container Service
VPCエンドポイント
VPCエンドポイントはFargateと異なりEC2からECSへ通信が必要になるため、以下のVPCエンドポイントを追加しています。
- com.amazonaws.${AWS::Region}.ecs-agent
- com.amazonaws.${AWS::Region}.ecs-telemetry
- com.amazonaws.${AWS::Region}.ecs
ECS
ECSは565~669行目あたりで設定しています。
重要な設定はキャパシティプロバイダになると思います。
AWS::ECS::CapacityProviderで設定しているのですが、ここでマネージドインスタンスが使用するインスタンスタイプなどが設定できます。
今回は特定のインスタンスタイプを設定はしておらず、指定したメモリやvCPUの範囲から選ばれるような設定にしています。(以下の部分です)
以下の設定だと、CPUアーキテクチャとしてintelもしくはamdのインスタンスタイプで、インスタンスサイズはメモリが1GB~5GB、vCPUが1~5の範囲のインスタンスサイズが使用されます。
また、BurstablePerformanceがincludedになっているとt系のバーストが行われるインスタンスタイプも含まれるようになります。
InstanceRequirements:
BurstablePerformance: included
CpuManufacturers:
- intel
- amd
MemoryMiB:
Max: 5120
Min: 1024
VCpuCount:
Max: 5
Min: 1
以下の設定はマネージドインスタンスが使用されていない (タスクが起動していない状態) でインスタンスが停止するまでの期間を設定しています。
最大で3600秒まで設定できますが、長ければ長いほど使用されていないインスタンスが動き続けるのでコストが上がります。
逆に短いとすぐに停止してしまうため、次にタスクが起動してきた際にEC2が起動していない状態となるためタスク起動に時間がかかることになります。
InfrastructureOptimization:
ScaleInAfter: 1800
動作確認
Webページの表示確認部分は割愛してタスクをスケーリングさせたときの動きを見てみます。
デプロイが完了すると以下のようにEC2インスタンスが起動します。(数台デプロイ失敗時の残骸が残っていますが実行中のものが使用されています)

また、クラスターのインフラストラクチャタブで起動しているEC2を確認することも可能です。

キャパシティプロバイダを確認するとEC2が使用する対象のインスタンスタイプも確認できます。(2ページ目にt3などバースト系のインスタンスタイプがリストされていました)

タスク数を増やしてみる
ということでタスク数を2台に増やしてみます。
ECSサービスのサービスの自動スケーリングタブから台数を変更します。

起動が完了するとすでに起動しているEC2内で起動していることが確認できます。
当初の想定としてはもう一台起動してマルチAZになるように動くと思っていたのですが、余裕があるインスタンス内で起動する動きになっているのかもしれません。

もう1台EC2を起動してほしいのでタスク数を倍の4台にしてみます。
4台起動してみたのですが、まだEC2に余裕があるらしくEC2は起動しませんでした。

さらに倍の8台にしてみます。
ここまで増やして新しいEC2が起動されました。

キャパシティプロバイダの画面で2台起動しているのが確認できます。
c6a.largeの方は最初に起動していたものなのですが、タスクとしてはもう1つくらいなら動かせそうな感じではありました。

上記の動きは以下のAWSブログで紹介されていました。
既存のインスタンス内に余裕がある場合はそこに置くような動きになっているようです。
複数インスタンスすでに起動している場合はAZ間でタスクを分散させてくれるみたいです。
EC2起動タイプにあるタスク配置戦略のbinpackみたいな動きなのかなと思います。
ECS はインスタンスごとに正確なリソース管理を行い、CPU、メモリ、その他のリソースをリアルタイムに追跡します。インスタンスが ECS に登録されると、利用可能な容量は EC2 インスタンスのリソース合計から ECS エージェントに予約されたメモリを差し引いた値になります。ECS はインスタンスの登録時に、インスタンスタイプごとの実績データに基づいてエージェントが消費するリソース量を推定し、複数の並行タスクに十分なリソースを確保します。タスクの起動と終了に伴い、ECS はインスタンスの利用可能なリソース容量を動的に更新します。新しいタスクの配置では、ECS は既存のインスタンスにリソースが十分にあるかどうかを最初にチェックします。複数の適切なインスタンスが存在する場合、ECS はマネージドインスタンスキャパシティプロバイダーで構成されたサブネットによって AZ 分散を制御しながら、最大の回復力を得るために AZ 間でタスクを分散させることを優先します。既存のインスタンスでは最適な AZ 分散ができなくても、現在の AZ に容量がある場合、ECS は新しいインスタンスをプロビジョニングするのではなく、サービスの目標タスク数に早く到達するために既存の容量を使用することを優先し、可用性を高めます。その後、ECS は継続的な AZ 間での ECS サービスの調整によってワークロードを調整し、可用性をさらに高めるための最適な AZ 分散を実現します。
最後に
EC2起動タイプと異なり現状 (2025年12月現在) タスク配置戦略が選択できないのは微妙な感じはありますが、EC2のパッチ管理などが不要なのはかなり強い要素だと思います。
また、EC2部分のAutoScalingもECS側に任せることができるのでここら辺の設定を省略できるのは楽でした。

