ECS (Fargate) サービスをHTTPSで公開するCloudFormationテンプレート

# はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
今回はNginxコンテナを`Fargate`でhttps対応させる`CloudFormation`テンプレートをご紹介します。
`CloudFormation`を使えばインフラの作成も削除も一瞬なので積極的に使えるようになりたいですよね。
完成像は下記を含みます。

* Route53 + ACM + ALBによるHTTPS設定
* 2つのAZへの負荷分散
* 1つのECS service(Fargate)
* FargateのAuto Scaling設定

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/d72cb3eb-0ccb-0ec5-b27b-f853053174ec.png)

# 前提条件
* AWS-CLIを設定済み
* ドメインを取得後、ネームサーバーの設定済（[参照](https://blog.i-tale.jp/2020/04/13_02/)）

# 1) IAMロールの作成
```create_IAM_roles.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: create IAM roles for ECS usage upfront
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
    Description: "A name that will be used for namespacing all cluster resources."

Resources:

  AutoscalingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: [application-autoscaling.amazonaws.com]
          Action: ['sts:AssumeRole']
      Path: /
      Policies:
      - PolicyName: service-autoscaling
        PolicyDocument:
          Statement:
          - Effect: Allow
            Action:
              - 'application-autoscaling:*'
              - 'cloudwatch:DescribeAlarms'
              - 'cloudwatch:PutMetricAlarm'
              - 'ecs:DescribeServices'
              - 'ecs:UpdateService'
            Resource: '*'
  
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: [ecs-tasks.amazonaws.com]
          Action: ['sts:AssumeRole']
      Path: /
      Policies:
        - PolicyName: AmazonECSTaskExecutionRolePolicy
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
                # Allow the ECS Tasks to download images from ECR
                - 'ecr:GetAuthorizationToken'
                - 'ecr:BatchCheckLayerAvailability'
                - 'ecr:GetDownloadUrlForLayer'
                - 'ecr:BatchGetImage'
                # Allow the ECS tasks to upload logs to CloudWatch
                - 'logs:CreateLogStream'
                - 'logs:PutLogEvents'
              Resource: '*'  

Outputs:
  AutoscalingRole:
    Description: The ARN of the role used for autoscaling
    Value: !GetAtt 'AutoscalingRole.Arn'
    Export:
      Name: !Sub ${EnvironmentName}:AutoscalingRole
  
  ECSTaskExecutionRole:
    Description: The ARN of the ECS role
    Value: !GetAtt 'ECSTaskExecutionRole.Arn'
    Export:
      Name: !Sub ${EnvironmentName}:ECSTaskExecutionRole
```
```
aws cloudformation create-stack --stack-name create-IAM-roles --capabilities CAPABILITY_IAM --template-body file://./create_IAM_roles.yml   
```

# 2) VPCの設定
```vpc_setup.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC and subnets as base for an ECS cluster
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course

Mappings:
  SubnetConfig:
    VPC:
      CIDR: '172.16.0.0/16'
    PublicOne:
      CIDR: '172.16.0.0/24'
    PublicTwo:
      CIDR: '172.16.1.0/24'

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']

  PublicSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: { Ref: 'AWS::Region' }
      VpcId: !Ref 'VPC'
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR']
      MapPublicIpOnLaunch: true
  PublicSubnetTwo:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: { Ref: 'AWS::Region' }
      VpcId: !Ref 'VPC'
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR']
      MapPublicIpOnLaunch: true

  InternetGateway:
    Type: AWS::EC2::InternetGateway
  GatewayAttachement:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref 'VPC'
      InternetGatewayId: !Ref 'InternetGateway'
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref 'VPC'
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: GatewayAttachement
    Properties:
      RouteTableId: !Ref 'PublicRouteTable'
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref 'InternetGateway'
  PublicSubnetOneRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetOne
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetTwoRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetTwo
      RouteTableId: !Ref PublicRouteTable

Outputs:
  DefaultRegion:
    Description: The default region setting on aws-cli
    Value: { Ref: 'AWS::Region' }
    Export:
      Name: !Sub ${EnvironmentName}:Region
  VpcId:
    Description: The ID of the VPC that this stack is deployed in
    Value: !Ref 'VPC'
    Export:
      Name: !Sub ${EnvironmentName}:VpcId
  PublicSubnetOne:
    Description: Public subnet one
    Value: !Ref 'PublicSubnetOne'
    Export:
      Name: !Sub ${EnvironmentName}:PublicSubnetOne
  PublicSubnetTwo:
    Description: Public subnet two
    Value: !Ref 'PublicSubnetTwo'
    Export:
      Name: !Sub ${EnvironmentName}:PublicSubnetTwo
```
```
aws cloudformation create-stack --stack-name vpc-setup --capabilities CAPABILITY_IAM --template-body file://./vpc_setup.yml 
```
# 3) ECSクラスターの設定
```ecs_cluster_setup.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS cluster launchtype Fargate.
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
    Description: 'A name that will be used for namespacing our cluster resources.'
  ClusterName:
    Type: String
    Default: ecs-course-fargate
    Description: 'A name that will be used for the ecs cluster.'
Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref 'ClusterName'
  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Access to the ECS hosts that run containers
      VpcId:
        Fn::ImportValue: !Sub ${EnvironmentName}:VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
Outputs:
  ECSCluster:
    Description: The name of the ECS cluster
    Value: !Ref 'ECSCluster'
    Export:
      Name: !Sub ${EnvironmentName}:ECSCluster
  ClusterName:
    Description: The name of the ECS cluster
    Value: !Ref 'ClusterName'
    Export:
      Name: !Sub ${EnvironmentName}:ClusterName
  ContainerSecurityGroup:
    Description: The container sg of the ECS cluster
    Value: !Ref 'ContainerSecurityGroup'
    Export:
      Name: !Sub ${EnvironmentName}:ContainerSecurityGroup
```
```
aws cloudformation create-stack --stack-name ecs-cluster-setup --capabilities CAPABILITY_IAM --template-body file://./ecs_cluster_setup.yml 
```

# 4) Route53 + ACM + ALB + ECS(Fargate)の設定
```https_nginx_setup.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: External, public facing load balancer, for forwarding public traffic to containers
Parameters:
  DomainName:
    Description: FQDN of the HostZone
    Type: String
    Default: 'example.com' #取得済のドメイン名
  SubDomain:
    Description: FQDN of the certificate
    Type: String
    Default: 'test.example.com' #サブドメインを指定
  HostZoneId:
    Description: FQDN of the hosted zone
    Type: String
    Default: 'Z05XXXXXXXXXXXXXX' # Route53のホストゾーンIDを記入
  EnvironmentName:
    Type: String
    Default: ecs-course
  ServiceName:
    Type: String
    Default: nginx-service
    Description: A name for the service
  ImageUrl:
    Type: String
    Default: nginx:1.17.7 #ECRのイメージも設定できる。（ECSTaskExecutionRoleにてECRが許可されている）
    Description:
      The url of a docker image that contains the application process that
      will handle the traffic for this service
  ContainerPort:
    Type: Number
    Default: 80
    Description: What port number the application inside the docker container is binding to
  ContainerCpu:
    Type: Number
    Default: 256
    Description: How much CPU to give the container. 1024 is 1 CPU
  ContainerMemory:
    Type: Number
    Default: 512
    Description: How much memory in megabytes to give the container
  Priority:
    Type: Number
    Default: 1
    Description: The priority for the routing rule added to the load balancer.
      This only applies if your have multiple services which have been
      assigned to different paths on the load balancer.
  DesiredCount:
    Type: Number
    Default: 1
    Description: How many copies of the service task to run
  Role:
    Type: String
    Default: ''
    Description:
      (Optional) An IAM role to give the service's containers if the code within needs to
      access other AWS resources like S3 buckets, DynamoDB tables, etc
Conditions:
  HasCustomRole: !Not [!Equals [!Ref 'Role', '']]
Resources:
  # A log group for storing the stdout logs from this service's containers
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${EnvironmentName}-service-${ServiceName}

  # The task definition. This is a simple metadata description of what
  # container to run, and what resource requirements it has.
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref 'ServiceName'
      Cpu: !Ref 'ContainerCpu'
      Memory: !Ref 'ContainerMemory'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn:
        Fn::ImportValue: !Sub ${EnvironmentName}:ECSTaskExecutionRole
      TaskRoleArn:
        Fn::If: #三項演算子。１行目がtrueなら２行目が参照され、falseなら３行目が参照される
          - 'HasCustomRole'
          - !Ref 'Role'
          - !Ref 'AWS::NoValue'
      ContainerDefinitions:
        - Name: !Ref 'ServiceName'
          Cpu: !Ref 'ContainerCpu'
          Memory: !Ref 'ContainerMemory'
          Image: !Ref 'ImageUrl'
          PortMappings:
            - ContainerPort: !Ref 'ContainerPort'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Sub ${EnvironmentName}-service-${ServiceName}
              awslogs-region: !Ref 'AWS::Region'
              awslogs-stream-prefix: !Ref 'ServiceName'

  # The service. The service is a resource which allows you to run multiple
  # copies of a type of task, and gather up their logs and metrics, as well
  # as monitor the number of running tasks and replace any that have crashed
  Service:
    Type: AWS::ECS::Service
    DependsOn: HTTPSLoadBalancerListener
    Properties:
      ServiceName: !Ref 'ServiceName'
      Cluster:
        Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref 'DesiredCount'
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - Fn::ImportValue: !Sub ${EnvironmentName}:ContainerSecurityGroup
          Subnets:
            - Fn::ImportValue: !Sub ${EnvironmentName}:PublicSubnetOne
            - Fn::ImportValue: !Sub ${EnvironmentName}:PublicSubnetTwo
      TaskDefinition: !Ref 'TaskDefinition'
      LoadBalancers:
        - ContainerName: !Ref 'ServiceName'
          ContainerPort: !Ref 'ContainerPort'
          TargetGroupArn: !Ref 'TargetGroup'
  ApiDomain:
    Type: AWS::Route53::RecordSet
    DependsOn: PublicLoadBalancer
    Properties:
      HostedZoneId: !Sub '${HostZoneId}'
      Name: !Sub '${SubDomain}'
      Type: CNAME
      TTL: 60
      ResourceRecords:
        - !GetAtt PublicLoadBalancer.DNSName
  ACMCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Sub '${SubDomain}'
      DomainValidationOptions:
        - DomainName: !Sub '${SubDomain}'
          HostedZoneId: !Sub '${HostZoneId}'
      ValidationMethod: DNS
  PublicLoadBalancerSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Access to the public facing load balancer
      VpcId:
        Fn::ImportValue: !Sub ${EnvironmentName}:VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
  PublicLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: '30'
      Subnets:
        - Fn::ImportValue: !Sub ${EnvironmentName}:PublicSubnetOne
        - Fn::ImportValue: !Sub ${EnvironmentName}:PublicSubnetTwo
      SecurityGroups: [!Ref 'PublicLoadBalancerSG']
  HTTPLoadBalancerListener: # 80=>443に転送する
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn:
      - PublicLoadBalancer
    Properties:
      DefaultActions:
        - Type: 'redirect'
          RedirectConfig:
            Protocol: 'HTTPS'
            Port: 443
            Host: '#{host}'
            Path: '/#{path}'
            Query: '#{query}'
            StatusCode: 'HTTP_301'
      LoadBalancerArn: !Ref 'PublicLoadBalancer'
      Port: 80
      Protocol: HTTP
  HTTPSLoadBalancerListener: # SSL設定してTargetGroupに転送
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn: PublicLoadBalancer
    Properties:
      LoadBalancerArn: !Ref 'PublicLoadBalancer'
      Port: 443
      Protocol: HTTPS
      DefaultActions:
        - Type: 'forward'
          TargetGroupArn: !Ref 'TargetGroup'
      SslPolicy: 'ELBSecurityPolicy-2016-08'
      Certificates:
        - CertificateArn: !Ref 'ACMCertificate'
  TargetGroup: # Serviceを参照させる
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: PublicLoadBalancer
    Properties:
      HealthCheckIntervalSeconds: 6
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      TargetType: ip #fargateなのでVPC内のipをtargetにする。EC2であればinstanceがtargetになる
      Name: !Ref 'ServiceName'
      Port: !Ref 'ContainerPort'
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      VpcId:
        Fn::ImportValue: !Sub ${EnvironmentName}:VpcId
Outputs:
  ExternalUrl:
    Description: The url of the external load balancer
    Value: !Sub https://${PublicLoadBalancer.DNSName}
    Export:
      Name: !Sub ${EnvironmentName}:ExternalUrl
  ServiceName:
    Description: The name of ecs service
    Value: !Ref 'ServiceName'
    Export:
      Name: !Sub ${EnvironmentName}:ServiceName
```
```
aws cloudformation create-stack --stack-name https-nginx-setup --capabilities CAPABILITY_IAM --template-body file://./https_nginx_setup.yml 
```

# 5) Auto Scaling Groupの設定
CPU使用率をモニタリングし、一定時間の平均値が連続で閾値を超えるとスケールアウト、閾値を下回るとスケールインします。

```auto-scaling-setup.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: AutoScaling setup for fargate service
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
    Description: 'A name that will be used for namespacing our cluster resources.'
  TaskMinContainerCount:
    Type: Number
    Description: Minimum number of containers to run for the service
    Default: 1
    MinValue: 1
    ConstraintDescription: Value must be at least one
  TaskMaxContainerCount:
    Type: Number
    Description: Maximum number of containers to run for the service when auto scaling out
    Default: 2
    MinValue: 1
    ConstraintDescription: Value must be at least one
  ServiceScaleEvaluationPeriods:
    Description: The number of periods over which data is compared to the specified threshold
    Type: Number
    Default: 2
    MinValue: 2
  ServiceCpuScaleOutThreshold:
    Type: Number
    Description: Average CPU value to trigger auto scaling out
    Default: 50
    MinValue: 0
    MaxValue: 100
    ConstraintDescription: Value must be between 0 and 100
  ServiceCpuScaleInThreshold:
    Type: Number
    Description: Average CPU value to trigger auto scaling in
    Default: 25
    MinValue: 0
    MaxValue: 100
    ConstraintDescription: Value must be between 0 and 100
Resources:
  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: !Ref TaskMinContainerCount
      MaxCapacity: !Ref TaskMaxContainerCount
      ResourceId:
        !Sub #この書き方により、コンテキスト内でのみ有効な変数を定義できる
        - service/${ECSClusterName}/${ECSServiceName} #auto scalingを適用するECSservieをこのフォーマットで指定。
        - ECSClusterName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
          ECSServiceName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      RoleARN:
        Fn::ImportValue: !Sub ${EnvironmentName}:AutoscalingRole
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs

  ServiceScaleOutPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub
        - ${EnvironmentName}-${ECSServiceName}-ScaleOutPolicy
        - EnvironmentName: !Ref EnvironmentName
          ECSServiceName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      PolicyType: StepScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - ScalingAdjustment: 1
            MetricIntervalLowerBound: 0
    DependsOn: ServiceScalingTarget

  ServiceScaleInPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub
        - ${EnvironmentName}-${ECSServiceName}-ScaleInPolicy
        - EnvironmentName: !Ref EnvironmentName
          ECSServiceName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      PolicyType: StepScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - ScalingAdjustment: -1
            MetricIntervalUpperBound: 0
    DependsOn: ServiceScalingTarget

  ServiceScaleOutAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub
        - ${EnvironmentName}-${ECSServiceName}-ScaleOutAlarm
        - EnvironmentName: !Ref EnvironmentName
          ECSServiceName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
      Statistic: Average
      TreatMissingData: notBreaching
      Threshold: !Ref ServiceCpuScaleOutThreshold
      AlarmDescription: Alarm to add capacity if CPU is high
      Period: 60
      AlarmActions:
        - !Ref ServiceScaleOutPolicy
      Namespace: AWS/ECS
      Dimensions:
        - Name: ClusterName
          Value:
            Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
        - Name: ServiceName
          Value: !Sub
            - ${EnvironmentName}-${ECSServiceName}
            - EnvironmentName: !Ref EnvironmentName
              ECSServiceName:
                Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      ComparisonOperator: GreaterThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ServiceScaleOutPolicy

  ServiceScaleInAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub
        - ${EnvironmentName}-${ECSServiceName}-ScaleInAlarm
        - EnvironmentName: !Ref EnvironmentName
          ECSServiceName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
      Statistic: Average
      TreatMissingData: notBreaching
      Threshold: !Ref ServiceCpuScaleInThreshold
      AlarmDescription: Alarm to reduce capacity if container CPU is low
      Period: 300
      AlarmActions:
        - !Ref ServiceScaleInPolicy
      Namespace: AWS/ECS
      Dimensions:
        - Name: ClusterName
          Value:
            Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
        - Name: ServiceName
          Value: !Sub
            - ${EnvironmentName}-${ECSServiceName}
            - EnvironmentName: !Ref EnvironmentName
              ECSServiceName:
                Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      ComparisonOperator: LessThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ServiceScaleInPolicy
```
```
aws cloudformation create-stack --stack-name auto-scaling-setup  --capabilities CAPABILITY_IAM --template-body file://./auto-scaling-setup.yml
```