ECSでマイクロサービスアーキテクチャを構築し、HTTPSで公開するCloudFormationテンプレート

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

# 全体像
ECSクラスタ内では、service discoveryを使って複数のservice間で通信を行うことができます。
今回は、下記のように簡単な2つのserviceを連携させます。

* `https://subdomain.domain.com` => Frontend ECS serviceから`Hello from FRONTEND-server!`が返ってくる
* `https://subdomain.domain.com/backend` => Frontend ECS serviceからBackend ECS serviceを介して`Hello from BACKEND-server!`が返ってくる

![ECS_CICD_pipeline-Page-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/5c4c22de-ee97-b034-2885-2d5e83fe9145.png)
※FargateとNat gatewayは時間課金となりますのでご注意ください。

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

# 1) フロントエンドアプリの作成
ビルドしてECRやDockerhubなど適当なレジストリにpushしてください。

```frontend-app.js
const express = require('express');
const app = express();
const port = 3000;
const axios = require('axios');
const url = 'http://node-service-backend.example-namespace.local:8000';

// backendサービスに繋ぐためには、後ほど作成するservice discoveryの設定が必要
// Route53でクラスター内のnamespaceを作り、そこにbackendサービスの場所を問い合わせる必要がある。
// エンドポイントは、ecsService.namespace:portとなる
// 今回は、backend ECS service名がnode-service-backendで、namespace名がexample-namespace.localとなる


app.get('/', (req, res) => {
  res.send('Hello from FRONTEND-server!');
});

app.get('/backend', async (req, res) => {
  const axios_res = await axios.get(url);
  res.send(axios_res.data);
});

app.listen(port, () => {
  console.log(`Example app listening at port:${port}`);
});
```
```Dockerfile.
FROM node:12
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY frontend-app.js .
EXPOSE 3000
CMD [ "node", "frontend-app.js" ]
```

# 2) バックエンドアプリの作成
ビルドしてECRやDockerhubなど適当なレジストリにpushしてください。

```backend-app.js
const express = require('express');
const app = express();
const port = 8000;

app.get('/', (req, res) => {
  res.send('Hello from BACKEND-server!');
});

app.listen(port, () => {
  console.log(`Example app listening at port:${port}`);
});
```
```Dockerfile.
FROM node:12
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY backend-app.js .
EXPOSE 8000
CMD [ "node", "backend-app.js" ]
```

# 3) IAMロールの作成
下記2つのロールを作成します

* ECSタスクを実行するためのロール
* オートスケールを実行するためのロール

```iam_role.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 --capabilities CAPABILITY_IAM --template-body file://./iam_role.yml
```

# 4) VPCの設定
VPCの設定をします。
2つのAZにそれぞれpublic/privateサブネットを1つづつ配置し、publicサブネットに配置したNatに対してprivateサブネットからルーティングします。`Nat gateway`は時間課金なので注意しましょう。
Nat（もしくは今回やらないPrivate link）を配置しないと、privateサブネットからS3やECR/Dockerhubなどにアクセスすることができません。

```vpc.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'
    PrivateOne:
      CIDR: '172.16.2.0/24'
    PrivateTwo:
      CIDR: '172.16.3.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

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

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

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName

  InternetGatewayAttachment:
    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: InternetGatewayAttachment
    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

  NatGateway1EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway2EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnetOne

  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway2EIP.AllocationId
      SubnetId: !Ref PublicSubnetTwo

  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ1)

  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnetOne

  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Routes (AZ2)

  DefaultPrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnetTwo

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
  PrivateSubnetOne:
    Description: Private subnet one
    Value: !Ref 'PrivateSubnetOne'
    Export:
      Name: !Sub ${EnvironmentName}:PrivateSubnetOne
  PrivateSubnetTwo:
    Description: Private subnet two
    Value: !Ref 'PrivateSubnetTwo'
    Export:
      Name: !Sub ${EnvironmentName}:PrivateSubnetTwo
```
```
aws cloudformation create-stack --stack-name VPC --capabilities CAPABILITY_IAM --template-body file://./vpc.yml
```

# 5) 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.'

Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref 'ClusterName'

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
```
```
aws cloudformation create-stack --stack-name ecs-cluster --capabilities CAPABILITY_IAM --template-body file://./ecs_cluster.yml
```

#6) フロントエンドアプリをfrontend serviceとしてデプロイする
publicサブネットにALBを配置してACMと連携させてHTTPS対応します。
ALBからprivateサブネットに配置したフロントエンドアプリにrequestを転送します。
事前に、（お名前.comなどで用意した）ドメイン名をRoute53にホストしておいてください。
詳細は[こちら](https://blog.i-tale.jp/2020/04/13_02/)

```frontend_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: mydomain.com'
  SubDomain:
    Description: FQDN of the certificate
    Type: String
    Default: 'subdomain.mydomain.com'
  HostZoneId:
    Description: FQDN of the hosted zone
    Type: String
    Default: 'Z0XXXXXXXXXXXXX' # Route53のホストゾーン名
  EnvironmentName:
    Type: String
    Default: ecs-course
  ServiceName:
    Type: String
    Default: node-service
    Description: A name for the service
  ImageUrl:
    Type: String
    Default: <your-image-url> # フロントアプリのDockerイメージのurl
    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'

  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内からの通信のみ許可

  # 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 # これによりECRにアクセスできる
          SecurityGroups:
            - !Ref ContainerSecurityGroup
          Subnets:
            - Fn::ImportValue: !Sub ${EnvironmentName}:PrivateSubnetOne
            - Fn::ImportValue: !Sub ${EnvironmentName}:PrivateSubnetTwo
      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 frontend-service --capabilities CAPABILITY_IAM --template-body file://./frontend_service.yml
```

# 7) frontend serviceのオートスケーリング設定
```auto_scaling.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.yml
```

# 8) バックエンドアプリをbackend serviceとしてデプロイして、frontend serviceとつなげる
frontend serviceからbackend serviceに接続するためには、service discoveryが必要となります。

```backend_service.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: External, public facing load balancer, for forwarding public traffic to containers
Parameters:
  EnvironmentName:
    Type: String
    Default: ecs-course
  NameSpace:
    Type: String
    Default: example-namespace.local
    Description: A name for the namespace
  ServiceName:
    Type: String
    Default: node-service-backend
    Description: A name for the service
  ImageUrl:
    Type: String
    Default: <your-image-url> # バックエンドアプリのDockerイメージのurl
    Description:
      The url of a docker image that contains the application process that
      will handle the traffic for this service
  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
  ContainerPort:
    Type: Number
    Default: 8000
    Description: What port number the application inside the docker container is binding to
  DesiredCount:
    Type: Number
    Default: 1
    Description: How many copies of the service task to run
  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:
  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内からの通信のみ許可

  PrivateNamespace: #route53でnamespaceを作成
    Type: AWS::ServiceDiscovery::PrivateDnsNamespace
    Properties:
      Name: !Ref NameSpace
      Vpc:
        Fn::ImportValue: !Sub ${EnvironmentName}:VpcId

  DiscoveryService: #namespaceとserviceを紐付ける
    Type: AWS::ServiceDiscovery::Service
    Properties:
      Description: Discovery Service for the Demo Application
      DnsConfig:
        RoutingPolicy: MULTIVALUE
        DnsRecords:
          - TTL: 60
            Type: A
          - TTL: 60
            Type: SRV
      HealthCheckCustomConfig:
        FailureThreshold: 1
      Name: !Ref ServiceName
      NamespaceId: !Ref PrivateNamespace

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${EnvironmentName}-service-${ServiceName}

  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: !Ref 'AWS::NoValue' #とりあえず。本来はDBへの接続権限とか
      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'

  Service:
    Type: AWS::ECS::Service
    DependsOn: TaskDefinition
    Properties:
      ServiceName: !Ref 'ServiceName'
      Cluster:
        Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref 'DesiredCount'
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED # これによりECRにアクセスできる
          SecurityGroups:
            - !Ref ContainerSecurityGroup
          Subnets:
            - Fn::ImportValue: !Sub ${EnvironmentName}:PrivateSubnetOne
            - Fn::ImportValue: !Sub ${EnvironmentName}:PrivateSubnetTwo
      TaskDefinition: !Ref 'TaskDefinition'
      ServiceRegistries:
        - RegistryArn: !GetAtt DiscoveryService.Arn
          Port: !Ref 'ContainerPort'

  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    DependsOn: Service
    Properties:
      MinCapacity: !Ref TaskMinContainerCount
      MaxCapacity: !Ref TaskMaxContainerCount
      ResourceId:
        !Sub #この書き方により、コンテキスト内でのみ有効な変数を定義できる
        - service/${ECSClusterName}/${ECSServiceName} #auto scalingを適用するECSservieをこのフォーマットで指定。
        - ECSClusterName:
            Fn::ImportValue: !Sub ${EnvironmentName}:ECSCluster
          ECSServiceName: !Ref 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: !Ref 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: !Ref 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: !Ref 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: !Ref ServiceName
      ComparisonOperator: GreaterThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ServiceScaleOutPolicy

  ServiceScaleInAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub
        - ${EnvironmentName}-${ECSServiceName}-ScaleInAlarm
        - EnvironmentName: !Ref EnvironmentName
          ECSServiceName: !Ref 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: !Ref ServiceName
      ComparisonOperator: LessThanThreshold
      MetricName: CPUUtilization
    DependsOn:
      - ServiceScaleInPolicy
```
```
aws cloudformation create-stack --stack-name backend-service --capabilities CAPABILITY_IAM --template-body file://./backend_service.yml
```
# 参考にさせていただいたもの

https://github.com/laser/cloudformation-fargate-codepipeline-ecs-refarch