目次

Introduction

この記事ではコンテナサービスであるECSを使用してWebサーバーを構築します。今回も前回と同じく多くのCFnを作成するため長くなります。頑張ってみていただけると嬉しいです。

今回作成するもの

AWSリソース

CFn相関図

HandsOn

ECSで使用するIAM Role

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml(modify)
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: iam-role. AWS::IAM::Role AWS::IAM::InstanceProfile"

Resources:
  CloudTrailRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cloudtrail-role
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: cloudtrail-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: CloudWatchPutPolicy
                Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*

  ECSInstanceProfileRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ecs-instance-profile-role
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role

  ECSInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref ECSInstanceProfileRole

  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: task-execution-role
      Path: /service-role/
      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

Outputs:
  CloudTrailRoleArn:
    Description: Output Export AWS::IAM::Role.Arn CloudTrailRole
    Value: !GetAtt CloudTrailRole.Arn
    Export:
      Name: cloudtrail-role-arn
  ECSInstanceProfileArn:
    Description: Output Export AWS::IAM::InstanceProfile.Arn ECSInstanceProfile
    Value: !GetAtt ECSInstanceProfile.Arn
    Export:
      Name: ecs-instance-profile-arn
  TaskExecutionRoleArn:
    Description: Output Export AWS::IAM::Role.Arn TaskExecutionRole
    Value: !GetAtt TaskExecutionRole.Arn
    Export:
      Name: task-execution-role-arn

ECSのコンテナインスタンスのIAMロールであるECSInstanceProfileとタスクを実行するためのIAMロールであるTaskExecutionRoleの2つを作成します。 ECSInstanceProfileは、Amazon ECS コンテナインスタンスの IAM ロールを参考にしました。今回は、EC2で構築するECSのためコンテナインスタンスロールを必要としますが、Fargateの場合は必要ないみたいです。
タスク実行ロールのTaskExecutionRoleはタスク定義の作成で使用します。
各ECSロールの解説は、【ECS】ECSに関するIAM Roleを整理する【AWS】をご覧ください

ECS Cluster

ECSのクラスターを作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-cluster. AWS::ECS::Cluster AWS::EC2::LaunchTemplate AWS::AutoScaling::AutoScalingGroup"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Name Configuration
        Parameters:
          - Prefix
          - Env
          - Project
      - Label:
          default: Network And Security Configuration
        Parameters:
          - SubnetId
          - SecurityGroup
      - Label:
          default: AutoScalingGroup Parameter Configuration
        Parameters:
          - Priority1InstanceType
          - Priority2InstanceType
          - Priority3InstanceType
          - AsgMaxSize
          - AsgMinSize
          - AsgDesiredSize
          - AsgOnDemandBase
          - AsgOnDemandPercentage
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example
  SubnetId:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Subnet for ECSInstance
  SecurityGroup:
    Type: List<AWS::EC2::SecurityGroup::Id>
    Description: SecurityGroup for ECSInstance
  Priority1InstanceType:
    Type: String
    Description: Allowed ECS InstanceType. priority 1
    AllowedValues: [t3.micro, t2.micro, t3.small]
  Priority2InstanceType:
    Type: String
    Description: Allowed ECS InstanceType. priority 2
    AllowedValues: [t3.micro, t2.micro, t3.small]
  Priority3InstanceType:
    Type: String
    Description: Allowed ECS InstanceType. priority 3
    AllowedValues: [t3.micro, t2.micro, t3.small]
  AsgMaxSize:
    Type: Number
    Description: AutoScaling maximum number of ECSInstance
    Default: 2
  AsgMinSize:
    Type: Number
    Description: AutoScaling minimum number of ECSInstance
    Default: 1
  AsgDesiredSize:
    Type: Number
    Description: AutoScaling desired number of ECSInstance
    Default: 1
  AsgOnDemandBase:
    Type: Number
    Description: "Minimum number of activations on-demand ECSInstance"
    Default: 0
  AsgOnDemandPercentage:
    Type: Number
    Description: Percentage of on-demand ECSInstance(0~100%)
    Default: 0
    MaxValue: 100
    MinValue: 0

Mappings:
  EnvMaps:
    dev:
      EbsVolume: 32
      AMIId: ami-02378d43835d39ff4 # ecs_agent_version: 1.68.2 / image_version: 2.0.20230214
    prd:
      EbsVolume: 32
      AMIId: ami-02378d43835d39ff4 # ecs_agent_version: 1.68.2 / image_version: 2.0.20230214

Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${Prefix}-${Env}-${Project}-cluster

  ECSLT:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub ${Prefix}-${Env}-${Project}-lt
      LaunchTemplateData:
        SecurityGroupIds: !Ref SecurityGroup
        InstanceInitiatedShutdownBehavior: terminate
        IamInstanceProfile:
          Arn: !ImportValue ecs-instance-profile-arn
        BlockDeviceMappings:
          - DeviceName: /dev/xvda
            Ebs:
              VolumeSize: !FindInMap [EnvMaps, !Ref Env, EbsVolume]
              DeleteOnTermination: true
              VolumeType: gp3
        DisableApiTermination: false
        ImageId: !FindInMap [EnvMaps, !Ref Env, AMIId]
        UserData:
          Fn::Base64: !Sub
            - |
              #!/bin/bash
              echo ECS_CLUSTER=${ECS_CLUSTER} >> /etc/ecs/ecs.config              
            - ECS_CLUSTER: !Ref ECSCluster

  ECSASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    UpdatePolicy:
      AutoScalingRollingUpdate:
        MaxBatchSize: 1
        MinInstancesInService: 1
        PauseTime: PT2M
    Properties:
      AutoScalingGroupName: !Sub ${Prefix}-${Env}-${Project}-asg
      MixedInstancesPolicy:
        InstancesDistribution:
          OnDemandBaseCapacity: !Ref AsgOnDemandBase
          OnDemandPercentageAboveBaseCapacity: !Ref AsgOnDemandPercentage
        LaunchTemplate:
          LaunchTemplateSpecification:
            LaunchTemplateId: !Ref ECSLT
            Version: !GetAtt ECSLT.LatestVersionNumber
          Overrides:
            - InstanceType: !Ref Priority1InstanceType
            - InstanceType: !Ref Priority2InstanceType
            - InstanceType: !Ref Priority3InstanceType
      MetricsCollection:
        - Granularity: 1Minute
      VPCZoneIdentifier: !Ref SubnetId
      MinSize: !Ref AsgMinSize
      MaxSize: !Ref AsgMaxSize
      DesiredCapacity: !Ref AsgDesiredSize
      Tags:
        - Key: Name
          Value: !Sub ${Prefix}-${Env}-${Project}-ecs-instance
          PropagateAtLaunch: true

Outputs:
  ECSClusterArn:
    Description: Output Export AWS::ECS::Cluster.Arn
    Value: !GetAtt ECSCluster.Arn
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-cluster-arn
  ECSClusterName:
    Description: Output Export AWS::ECS::Cluster.Ref
    Value: !Ref ECSCluster
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-cluster-name

スタック名は、v1-dev-ecs-cluster
Parametersの説明は以下になります。

  • SubnetId: PrivateなSubnetを選択してください。
  • SecurityGroup: ECSインスタンス用のsgを選択
  • Priority(x)InstanceType: ECSで立てるEC2のインスタンスタイプです。
    1~3で立ち上がる優先順位が選択でき、1が一番優先順位が高いです。今回のテンプレートではかなり小さいインスタンスタイプのみ選択できるようにしていますが、場合によって選択できるタイプを増やしていただければ、ただむやみにでかいインスタンスタイプを選択すると莫大な料金の請求がくるため気をつけてください
  • AsgMaxSize: AutoScalingで増加するインスタンスの最大数。
    今回は設定してないので意味をなしていない
  • AsgMinSize: AutoScalingで減少するインスタンスの最小数。
    今回は設定してないので意味をなしていない
  • AsgDesiredSize: AutoScalingで希望するインスタンス数。
    基本的にECSインスタンス数はここに入力した値の数になる
  • AsgOnDemandBase: オンデマンドインスタンスの最小数
  • AsgOnDemandPercentage: オンデマンドインスタンスの存在パーセンテージ AsgOnDemandBaseと調整しながら設定していく。

今回は100%スポットインスタンスにして料金を抑えたいため、以下の画像の設定にします。

ここでは説明しないですが、AutoScalingの設定は少し複雑なため、AWS公式のUpdatePolicy 属性やDevelopersIOのCloudFormation UpdatePolicyを使用してAuto Scaling Groupの更新を処理するを見てみてください。

CloudWatchLogs

ECSで動くアプリケーションログを保存するためのCloudWatchLogsを作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      cw-logs.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-cw-logs. AWS::Logs::LogGroup"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Environment Name Configuration"
        Parameters:
          - Prefix
          - Env
          - Project

Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example

Resources:
  ECSCWLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ecs/${Prefix}-${Env}-${Project}
      RetentionInDays: 7

Outputs:
  ECSCWLogGroupName:
    Description:  Output Export AWS::Logs::LogGroup.Ref ECSCWLogGroup
    Value: !Ref ECSCWLogGroup
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name

スタック名は、v1-dev-cw-logs
ログの保存期間は7日間にします。

ECS TaskDefinition

次にタスク定義を作成します。
タスク定義は設定項目がかなり多いので今回は必要最低限の項目のみ記載します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml(new)
      cw-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-task-def. AWS::ECS::TaskDefinition"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Name Configuration
        Parameters:
          - Prefix
          - Env
          - Project
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example

Mappings:
  EnvMaps:
    dev:
      Cpu: 256
      Memory: 516
    prd:
      Cpu: 256
      Memory: 516

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${Prefix}-${Env}-${Project}-task
      RuntimePlatform:
        CpuArchitecture: X86_64
      RequiresCompatibilities:
        - EC2
      Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
      Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
      ExecutionRoleArn: !ImportValue task-execution-role-arn
      ContainerDefinitions:
        - Name: !Sub ${Prefix}-${Env}-${Project}-container
          Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
          Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
          Image:
            Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecr-uri
          PortMappings:
            - ContainerPort: 80
              HostPort: 0
              Protocol: tcp
          Essential: true
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:
                Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name
              awslogs-region: ap-northeast-1

スタック名は、v1-dev-ecs-task-def
コンテナのイメージは、まだGithubからコードを取ってくるリソースを作成していないため一時的にnginxの最新イメージを指定します。

ECS Service

ECSのタスク定義を作成したので、次にそれを使用してコンテナを立ち上げるECS Serviceを作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml(new)
      cw-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-service. AWS::ECS::Service"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Environment Name Configuration"
        Parameters:
          - Prefix
          - Env
          - Project
      - Label:
          default: "ECS Service Parameter Configuration"
        Parameters:
          - DesiredCount
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example
  DesiredCount:
    Type: Number
    Description: ECS Service task DesiredCount
    Default: 1
  MaximumPercent:
    Type: Number
    Description: ECS Service task max percentage
    Default: 100
  MinimumHealthyPercent:
    Type: Number
    Description: ECS Service task min percentage
    Default: 0

Resources:
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: !Sub ${Prefix}-${Env}-${Project}-service
      Cluster:
        Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-cluster-arn
      DeploymentConfiguration:
        MaximumPercent: !Ref MaximumPercent
        MinimumHealthyPercent: !Ref MinimumHealthyPercent
      DesiredCount: !Ref DesiredCount
      HealthCheckGracePeriodSeconds: 0
      LaunchType: EC2
      LoadBalancers:
        - TargetGroupArn:
            Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-tg-arn
          ContainerPort: 80
          ContainerName: !Sub ${Prefix}-${Env}-${Project}-container
      SchedulingStrategy: REPLICA
      TaskDefinition: !Sub ${Prefix}-${Env}-${Project}-task # リビジョンで固定したくないためImportValueを使用しない
      DeploymentController:
        Type: ECS

Outputs:
  ECSServiceArn:
    Description:  Output Export AWS::ECS::Service.Ref ECSService
    Value: !Ref ECSService
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-service-name
  ECSServiceName:
    Description:  Output Export AWS::ECS::Service.Ref ECSService
    Value: !GetAtt ECSService.Name
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-service-arn

スタック名は、v1-dev-ecs-service
ECS Serviceでのポイントは、DeploymentConfigurationです。この値はデプロイ時やリビジョン変更時にタスクをどの程度まで増減させるか決める設定になります。通常運用では半分まで減少を許容し最大で2倍増加することを求める設定をします(Max: 200, Min: 50)。今回はタスクが入れ替わる際、タスクが0になってよい設定にするのでMax: 100, Min: 0にします。
また、TaskDefinitionはImportValueでARNを利用しようとするとリビジョンも指定しないといけないのでSub関数でリビジョンは指定しないようにして最新のリビジョンで動くようにします。

これでnginxのコンテナが立ちました。 アクセスしてみましょう。

$ curl https://looseller.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

ブラウザだと画像のような画面になります。

終わりに

Webサービスが動くところまで作成できました。ECSサービスだけでも多くのサービスがあるため初めて触れる人は理解が大変だと思いますが、構築して上で理解できていくと思うので根気強くやっていただければと思います。
ついに次の記事で一旦最後になります。ゴールデンウィークでCFnの記事を書き切ろうと持って思っていましたが、ちょっとオーバーしそうです…

参考