ECS (Fargate) サービスをHTTPSで公開し、CodePipelineでCI/CD環境を構築するCloudFormationテンプレート

# はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
自分の学習記録としてまとめます。

CDKを使いたい人はこちら↓

https://qiita.com/theFirstPenguin/items/2c567cc1cdf763ca35db

# 全体像
下図のような開発環境をバシっとCloudFormationでつくります。
Githubのmainブランチに変更をpushするたびに自動でデプロイされます。
![ECS_CICD_pipeline-Page-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/f73f8b50-fe34-eac0-8031-28094f1ffa96.png)


※Docker Hubアカウントにログインせずにビルドを行っているため、Docker Hubの制限に引っかかる場合があります。詳細は[こちら](https://fu3ak1.hatenablog.com/entry/2020/11/22/122241)
↑ Docker Hubアカウントにログインしなくても、NatGWに関連付けされたプライベートサブネット内でビルドを行えば問題ありません。

※※頻繁にStackの削除を行うと、ACM証明書の上限に引っかかります。詳細は[こちら](https://dev-labo.com/aws/acm-upper-limit/)

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

# 1) ECRのレポジトリを作成
これ以降、Regionは`us-east-2`になってます。レポジトリ名は`ecr-test`にしておきます。

```ecr.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: make ECR repo.
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
    Description: 'A name that will be used for namespacing our cluster resources.'
  ECRName:
    Type: String
    Default: ecr-test
    Description: 'A name that will be used for this ecr'

Resources:
  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref ECRName

Outputs:
  Repository:
    Description: Test Repository
    Value: !Ref Repository
    Export:
      Name: !Sub ${EnvironmentName}:ECRName
```

```
aws cloudformation create-stack --stack-name ecr --capabilities CAPABILITY_IAM --template-body file://./ecr.yml
```

# 2) Node.js APIを作成
今回デプロイする簡単なAPIです。ポートが3000ということだけチェックしておいて下さい。

```Docker/app.js
const http = require('http');

var app = function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('HELLO WORLD\n');
};

http.createServer(app).listen(3000);
```

ビルドの設定です。

```Docker/Dockerfile.
FROM node:12

WORKDIR /usr/src/app

COPY app.js .

EXPOSE 3000
CMD [ "node", "app.js" ]
```

# 3) APIをビルドしてECRにpush
ビルドします。

```
docker build -t node-api ./Docker
```

ECRの認証をします。

```
aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin <your-aws-account-number>.dkr.ecr.us-east-2.amazonaws.com
```

ビルドしたイメージにタグをつけます。このタグがイメージの名前となります。
取り合えず最初のpushなので`init`としておきます。

```
docker tag node-api <your-aws-account-number>.dkr.ecr.us-east-2.amazonaws.com/ecr-test:init
```

タグで指定してECRにpushします。

```
docker push <your-aws-account-number>.dkr.ecr.us-east-2.amazonaws.com/ecr-test
```

# 4) Node.jsのAPIのECS (Fargate) サービスをHTTPSで公開
ここまでバッとやってしまいます。
![ECS_CICD_pipeline-Page-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/1b13a8f8-1df9-1804-95cd-08ffca8dfd57.png)



## 4-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:
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      # 下記ManagedPolicyArnsとPoliciesは同義。ただし下のように実際に書き出した方がわかりやすい
      # ManagedPolicyArns:
      # - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      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: '*'

  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: '*'

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 ecs-iam-role-setting --capabilities CAPABILITY_IAM --template-body file://./create_IAM_roles.yml
```

## 4-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 ecs-core-infrastructure --capabilities CAPABILITY_IAM --template-body file://./vpc-setup.yml
```

## 4-3) ECSクラスターの設定
```ecs-cluster.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.'
  ContainerPort:
    Type: Number
    Default: 3000 #nodeの解放ポートに合わせます
    Description: What port number the application inside the docker container is binding to
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: !Ref 'ContainerPort'
          CidrIp: 172.16.0.0/16 #VPC内からの通信のみ許可
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 --capabilities CAPABILITY_IAM --template-body file://./ecs-cluster.yml
```

## 4-4) Route53 + ACM + ALB + ECSの設定
```https-node-service.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のホストゾーン
  EnvironmentName:
    Type: String
    Default: ecs-course
  ServiceName:
    Type: String
    Default: node-service
    Description: A name for the service
  ImageUrl:
    Type: String
    Default: <your-aws-account-number>.dkr.ecr.us-east-2.amazonaws.com/ecr-test:init #最初のイメージ
    Description:
      The url of a docker image that contains the application process that
      will handle the traffic for this service
  ContainerPort:
    Type: Number
    Default: 3000
    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'
          Essential: true
          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-node-service --capabilities CAPABILITY_IAM --template-body file://./https-node-service.yml
```

## 4-5) オートスケーリングの設定
```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 --capabilities CAPABILITY_IAM --template-body file://./auto_scaling_setup.yml
```

# 5) CodePipelineの設定
↓本記事の内容は古いのでGithub連携については下記参照

https://qiita.com/theFirstPenguin/items/996f1b7bca92837c9f20

```code-pipeline.yml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
    Description: 'A name that will be used for namespacing our cluster resources.'
  GitHubRepositoryName:
    Type: String
    Default: <your-github-repo-name> #設定要
  GitHubAccountName:
    Type: String
    Default: <your-github-account-name> #設定要
  GitHubPipelineWebhookName:
    Type: String
    Default: <inut-your-unique-webhook-name> # レポジトリ内の他のwebhookと被らないようにする
  Branch:
    Type: String
    Default: <input-your-Github-branch-for-CodePipeline> #設定要
  GitHubOAuthToken: #別途パラメータ入力要
    Type: String
    NoEcho: true #本来secretをパラメータで渡すのは良くないが、この方法ならばパラメータ画面に表示されない

Resources:
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: codebuild.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      Policies:
        - PolicyName: code-build-service
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:ValidateTemplate
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - ecr:GetAuthorizationToken
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource: '*'
      Path: /

  # アプリケーションのテストやビルドの定義（開発環境）
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: deploy-test-build
      ServiceRole: !Ref CodeBuildServiceRole
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Type: LINUX_CONTAINER
        Image: aws/codebuild/docker:18.09.0-1.7.0
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: REPOSITORY_URI
            Value: !Sub
              - ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
              - Repository:
                  Fn::ImportValue: !Sub ${EnvironmentName}:ECRName
          - Name: CONTAINER_NAME
            Value:
              Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: buildspec.yml

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: '*'
                Effect: Allow
                Action:
                  - ecs:DescribeServices
                  - ecs:DescribeTaskDefinition
                  - ecs:DescribeTasks
                  - ecs:ListTasks
                  - ecs:RegisterTaskDefinition
                  - ecs:UpdateService
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                  - iam:PassRole

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket

  # 外部イベント発生のwebhook
  PipelineWebhook:
    Type: 'AWS::CodePipeline::Webhook'
    Properties:
      Authentication: GITHUB_HMAC
      AuthenticationConfiguration:
        SecretToken: !Ref GitHubOAuthToken
      Filters:
        - JsonPath: '$.ref'
          MatchEquals: !Sub
            - refs/heads/${Branch}
            - Branch: !Ref Branch
        - JsonPath: '$.created'
          MatchEquals: false
        - JsonPath: '$.deleted'
          MatchEquals: false
        # 特定のBranchの変更(push)を検知してwebhookを実行。このときBranchの作成と消去は無視する。
        # 詳しくは、Githubの Settings => Webhooks => Recent Deliveries => Payload を見ればわかる。
      TargetPipeline: !Ref Pipeline
      TargetAction: SourceAction
      Name: !Ref GitHubPipelineWebhookName
      TargetPipelineVersion: !GetAtt Pipeline.Version
      RegisterWithThirdParty: 'true'

  # 継続的デプロイに必要な権限
  # CodePipeLine
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: test-pipeline-for-node-pjt
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: 1
                Provider: GitHub
              Configuration:
                Owner: !Ref GitHubAccountName
                Repo: !Ref GitHubRepositoryName
                PollForSourceChanges: false #ポーリング処理はせず、webhookで動かす。
                Branch: !Ref Branch
                OAuthToken: !Ref GitHubOAuthToken
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceCode
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuild
              RunOrder: 1
              InputArtifacts:
                - Name: SourceCode
              OutputArtifacts:
                - Name: BuildOutput
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: ECS
              Configuration:
                ClusterName:
                  Fn::ImportValue: !Sub ${EnvironmentName}:ClusterName
                ServiceName:
                  Fn::ImportValue: !Sub ${EnvironmentName}:ServiceName
                FileName: image.json
              InputArtifacts:
                - Name: BuildOutput
              RunOrder: 1
```

```buildspec.yml
version: 0.2

phases:
  pre_build:
    commands:
      - IMAGE_URI="${REPOSITORY_URI}:$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)"
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
  build:
    commands:
      - echo Build started on $(date)
      - DEFAULT=`pwd`
      - cd ./Docker
      - docker build --tag ${IMAGE_URI} .
      - docker push ${IMAGE_URI}
      - cd ${DEFAULT}
  post_build:
    commands:
      - echo Build completed on $(date)
      - printf '[{"name":"%s","imageUri":"%s"}]' "${CONTAINER_NAME}" "$IMAGE_URI" > image.json
      # jsonのキーのnameとimageUriは必須。必要であればparametersを追加。

artifacts:
  files: image.json
```

```
aws cloudformation create-stack --stack-name code-pipeline --capabilities CAPABILITY_IAM --template-body file://./code-pipeline.yml --parameters ParameterKey=GitHubOAuthToken,ParameterValue=ghp_XXXXXXXXXX
```

# 参考にさせていただいたもの
* [AWS公式](https://github.com/aws-samples/ecs-refarch-cloudformation)
* [CodePipelineでCloudFormationを利用してECSをデプロイ](https://qiita.com/YutaSaito1991/items/1a001f0e19e9de033612)
* [Docker Hub の Rate Limitに引っかかったのでdocker loginで対策した](https://fu3ak1.hatenablog.com/entry/2020/11/22/122241)
* [ACMへの証明書アップロード数量制限と上限緩和と確認](https://dev-labo.com/aws/acm-upper-limit/)
* [Udemy](https://courses.datacumulus.com/downloads/amazon-ecs-95g/)
* [ECS Fargate 楽々構築テンプレート](https://note.com/dd_techblog/n/n109b59faa5f5)