0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECSマネージドインスタンスを触ってみた

Last updated at Posted at 2025-12-13

2025年9月頃に発表されたECSのマネージドインスタンスを触ってみたのでブログに残します。

ECSマネージドインスタンスとは

ECSの起動タイプの1つでAWS側に管理されたEC2インスタンス上でタスクを動かすことができる機能です。
今までECSではFargateEC2上でタスクを動かすことができました。
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かマネージドインスタンスかの違いくらいでここら辺は殆ど同じ構成でいけます。
ECSマネージドインスタンス.drawio.png

設定

少し調べたところ基本的には前回作成したブログの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から作成する形になります。

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インスタンスが起動します。(数台デプロイ失敗時の残骸が残っていますが実行中のものが使用されています)
スクリーンショット 2025-12-07 202201.png

また、クラスターのインフラストラクチャタブで起動しているEC2を確認することも可能です。
スクリーンショット 2025-12-07 202426.png

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

タスク数を増やしてみる

ということでタスク数を2台に増やしてみます。
ECSサービスのサービスの自動スケーリングタブから台数を変更します。
スクリーンショット 2025-12-07 203215.png

スクリーンショット 2025-12-07 203331.png

起動が完了するとすでに起動しているEC2内で起動していることが確認できます。
当初の想定としてはもう一台起動してマルチAZになるように動くと思っていたのですが、余裕があるインスタンス内で起動する動きになっているのかもしれません。
スクリーンショット 2025-12-07 203457.png

もう1台EC2を起動してほしいのでタスク数を倍の4台にしてみます。
4台起動してみたのですが、まだEC2に余裕があるらしくEC2は起動しませんでした。
スクリーンショット 2025-12-07 204137.png

さらに倍の8台にしてみます。
ここまで増やして新しいEC2が起動されました。
スクリーンショット 2025-12-07 204350.png

キャパシティプロバイダの画面で2台起動しているのが確認できます。

スクリーンショット 2025-12-07 204520.png

c6a.largeの方は最初に起動していたものなのですが、タスクとしてはもう1つくらいなら動かせそうな感じではありました。
スクリーンショット 2025-12-07 204638.png

上記の動きは以下の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側に任せることができるのでここら辺の設定を省略できるのは楽でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?