CloudFormationを使ってAWS Fargateの環境を構築する

#はじめに
本記事では、AWS CloudFormation管理コンソールを使って、AWS Fargateの基本的な環境を構築する手順を説明しています。（初心者向け）

本記事で掲載しているテンプレートの最新版は、下記に置いてます。
https://github.com/okubo-t/aws-cloudformation
#構成図
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/fb8288d7-c13b-00d8-3eab-af541046b090.jpeg"　 width=50%>

#前提条件
下記の記事の構築手順で、VPCを構築していること。
[CloudFormationを使ってVPCを構築する]
(https://qiita.com/okubot55/items/b18a5dd5166f1ec2696c)

作業端末においては、AWS CLIとDockerの環境が構築済みであること。

PJPrefixの値は、同一にすること。

#構築手順
1 Dockerfile を準備します。

```
echo "FROM nginx:alpine" > Dockerfile
```

2 AWS ECSの管理コンソールで、リポジトリを作成します。
  ここでは、リポジトリ名は、nginx　と設定します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/ce33c9c4-8e00-b110-ec1a-f83d96581d50.png"　 width=90%>

3 作成されるとプッシュコマンドが表示されますので、メモしておきます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/ba894c8c-4c53-52fd-a23e-376b4d5e9140.png"　 width=90%>

4 AWS 認証情報を設定しておきます。

```
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXX     # アクセスキー
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXX  # シークレットアクセスキー
export AWS_DEFAULT_REGION=ap-northeast-1      # デフォルトリージョン 
```

5 レジストリに対して Docker クライアントを認証するために使用するログインコマンドを実行します。（手順3 でメモしたコマンドです。）

```
$(aws ecr get-login --no-include-email --region ap-northeast-1)
```
6 Docker イメージを構築します。（手順3 でメモしたコマンドです。）

```
docker build -t nginx .
```
7 このリポジトリにイメージをプッシュできるように、イメージにタグを付けます。（手順3 でメモしたコマンドです。）

```
docker tag nginx:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
```
8 このリポジトリにこのイメージをプッシュします。（手順3 でメモしたコマンドです。）

```
docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
```

9 AWS CloudFormation管理コンソールから、スタックの作成をクリックします。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/e0c3e7c3-f478-18c7-2dc2-7e669d1b399d.png"　 width=50%>

10 後述のテンプレートを選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/060a4709-622a-2394-de5f-ac2103045137.png"　 width=50%>

11 各パラメータを入力します。（<font color="Red">全て必須</font>）
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/80733696-9325-8f94-783c-d5cf917b27ea.png"　 width=70%>

|パラメータ名|用途|備考|
|:---|:---|:---|
|スタックの名前|テンプレートから作成するリソース一式の名前|例　prd-stack-vpc-20180801|
|PJPrefix|構築するプロジェクトの環境を識別するために各コンポーネントの先頭に付与する識別子|例 qiita-prd|
|InternetALBName|TagのNameに設定するALB名|alb(デフォルト)|
|TargetGroupName|TagのNameに設定するターゲットグループ名|tg(デフォルト)|
|ECSClusterName|ECSのクラスター名|cluster(デフォルト)|
|ECSTaskName|ECSのタスク定義名|task(デフォルト)|
|ECSTaskCPUUnit|タスクで使用するCPUのユニットの数|256(デフォルト)|
|ECSTaskMemory|タスクが使用するメモリの量|512(デフォルト)|
|ECSContainerName|ECSのコンテナ名|container(デフォルト)|
|ECSImageName|コンテナに使用するイメージ名|nginx(デフォルト)|
|ECSServiceName|ECSのサービス名|service(デフォルト)|
|ECSTaskDesiredCount|クラスターで実行する同時タスクの数|1（デフォルト）|

12 後続は、デフォルトのまま次へ次へで、作成します。

13 状況が CREATE COMPLETEになれば、ALBとFargateの環境の構築が完了です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/f919b748-e62b-9f47-0eed-a3ab499405f0.png"　 width=50%>

14 管理コンソールの下部の出力から、構築したALBの情報を確認できます。
 　　ここで、キーがALBDNSNameの値をメモしておきます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/dfaff7e2-e893-e74a-4ec1-72e0e7a3e279.jpeg"　 width=８0%>

15 EC2の管理コンソールのターゲットグループにアタッチされたコンテナのステータスがhealthyになっていることを確認します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/eb5835ac-5883-a350-7949-d19d544efba8.jpeg"　 width=100%>

16 ブラウザから、先ほどメモしたALBDNSNameの値にWebアクセスしてみます。
下記のような画面が表示されれば完了です。
例）
<img src="https://qiita-image-store.s3.amazonaws.com/0/283246/7ac81dea-a1e8-8795-3818-106bb92e353f.jpeg"　 width=70%>

#テンプレート
```ecs-fargate-01.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  Fargate for ECS Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "InternetALB Configuration"
        Parameters:
          - InternetALBName
          - TargetGroupName
      - Label:
          default: "Fargate for ECS Configuration"
        Parameters:
          - ECSClusterName
          - ECSTaskName
          - ECSTaskCPUUnit
          - ECSTaskMemory
          - ECSContainerName
          - ECSImageName
          - ECSServiceName
          - ECSTaskDesiredCount
        
    ParameterLabels:
      InternetALBName:
        default: "InternetALBName"
      TargetGroupName:
        default: "TargetGroupName"
      ECSClusterName:
        default: "ECSClusterName"
      ECSTaskName:
        default: "ECSTaskName"
      ECSTaskCPUUnit:
        default: "ECSTaskCPUUnit"
      ECSTaskMemory:
        default: "ECSTaskMemory"
      ECSContainerName:
        default: "ECSContainerName"
      ECSImageName:
        default: "ECSImageName"
      ECSServiceName:
        default: "ECSServiceName"
      ECSTaskDesiredCount:
        default: "ECSTaskDesiredCount"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String

#InternetALB
  InternetALBName:
    Type: String
    Default: "alb"

#TargetGroupName
  TargetGroupName:
    Type: String
    Default: "tg"

#ECSClusterName
  ECSClusterName:
    Type: String
    Default: "cluster"

#ECSTaskName
  ECSTaskName:
    Type: String
    Default: "task"

#ECSTaskCPUUnit
  ECSTaskCPUUnit:
    AllowedValues: [ 256, 512, 1024, 2048, 4096  ]
    Type: String
    Default: "256"

#ECSTaskMemory
  ECSTaskMemory:
    AllowedValues: [ 256, 512, 1024, 2048, 4096  ]
    Type: String
    Default: "512"

#ECSContainerName
  ECSContainerName:
    Type: String
    Default: "container"

#ECSImageName
  ECSImageName:
    Type: String
    Default: "nginx"

#ECSServiceName
  ECSServiceName:
    Type: String
    Default: "service"

#ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Type: Number
    Default: 1 

Resources:
# ------------------------------------------------------------#
#  SecurityGroup for ALB
# ------------------------------------------------------------#
  ALBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      GroupName: !Sub "${PJPrefix}-alb-sg"
#ECSContainerName
  ECSContainerName:
    Type: String
    Default: "container"

#ECSImageName
  ECSImageName:
    Type: String
    Default: "nginx"

#ECSImageName
  ECSServiceName:
    Type: String
    Default: "service"

#ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Type: Number
    Default: 1 

Resources:
# ------------------------------------------------------------#
#  SecurityGroup for ALB
# ------------------------------------------------------------#
  ALBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      GroupName: !Sub "${PJPrefix}-alb-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${PJPrefix}-alb-sg"
# Rule
      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"

# ------------------------------------------------------------#
#  SecurityGroup for ECS Service
# ------------------------------------------------------------#
  ECSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" }
      GroupName: !Sub "${PJPrefix}-ecs-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: !Sub "${PJPrefix}-ecs-sg"

# Rule
  ECSSecurityGroupIngress: 
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties: 
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !GetAtt [ ALBSecurityGroup, GroupId ] 
      GroupId: !GetAtt [ ECSSecurityGroup, GroupId ]

# ------------------------------------------------------------#
#  Target Group
# ------------------------------------------------------------#
  TargetGroup: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: { "Fn::ImportValue": !Sub "${PJPrefix}-vpc" } 
      Name: !Sub "${PJPrefix}-${TargetGroupName}"
      Protocol: HTTP
      Port: 80
      TargetType: ip

# ------------------------------------------------------------#
#  Internet ALB
# ------------------------------------------------------------#
  InternetALB: 
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties: 
      Name: !Sub "${PJPrefix}-${InternetALBName}"
      Tags: 
        - Key: Name
          Value: !Sub "${PJPrefix}-${InternetALBName}"
      Scheme: "internet-facing"
      LoadBalancerAttributes: 
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets: 
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
        - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }

  ALBListener: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref InternetALB
      Port: 80
      Protocol: HTTP

# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------# 
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${PJPrefix}-${ECSClusterName}"

# ------------------------------------------------------------#
#  ECS LogGroup
# ------------------------------------------------------------#
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/${PJPrefix}-ecs-group"

# ------------------------------------------------------------#
#  ECS TaskDefinition
# ------------------------------------------------------------#
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
      Family: !Sub "${PJPrefix}-${ECSTaskName}"
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE

#ContainerDefinitions
      ContainerDefinitions:
        - Name: !Sub "${PJPrefix}-${ECSContainerName}"
          Image: !Ref ECSImageName
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Ref PJPrefix
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80

# ------------------------------------------------------------#
#  ECS Service
# ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !Ref TargetGroup
          ContainerPort: 80
          ContainerName: !Sub "${PJPrefix}-${ECSContainerName}"
      NetworkConfiguration:
       AwsvpcConfiguration:
           AssignPublicIp: ENABLED
           SecurityGroups:
             - !Ref ECSSecurityGroup
           Subnets:
             - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-a" }
             - { "Fn::ImportValue": !Sub "${PJPrefix}-public-subnet-c" }
      ServiceName: !Sub "${PJPrefix}-${ECSServiceName}"
      TaskDefinition: !Ref ECSTaskDefinition

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------# 
Outputs:
#InternetALB
  ALBDNSName:
    Value: !GetAtt InternetALB.DNSName
    Export:
      Name: !Sub "${PJPrefix}-${InternetALBName}-dnsname"

#ECSClusterName
  ECSClusterName:
    Value: !Sub "${PJPrefix}-${ECSClusterName}"
    Export:
      Name: !Sub "${PJPrefix}-${ECSClusterName}-name"

#ECSClusterARN
  ECSClusterARN:
    Value: !GetAtt ECSCluster.Arn
    Export:
      Name: !Sub "${PJPrefix}-${ECSClusterName}-arn"

#ECSLogGroup
  ECSLogGroupName:
    Value: !Sub "/ecs/logs/${PJPrefix}-ecs-group"
    Export:
      Name: !Sub "${PJPrefix}-ecs-group-name"

  ECSLogGroupARN:
    Value: !GetAtt ECSLogGroup.Arn
    Export:
      Name: !Sub "${PJPrefix}-ecs-group-arn"

#ECSTaskDefinitionName
  ECSTaskDefinitionName:
    Value: !Sub "${PJPrefix}-${ECSTaskName}"
    Export:
      Name: !Sub "${PJPrefix}-${ECSTaskName}-name"

#ECSTaskDefinition
  ECSTaskDefinitionARN:
    Value: !Ref ECSTaskDefinition
    Export:
      Name: !Sub "${PJPrefix}-${ECSTaskName}-arn"

#ECSTaskCPUUnit
  ECSTaskCPUUnit:
    Value: !Ref ECSTaskCPUUnit
    Export:
      Name: !Sub "${PJPrefix}-${ECSTaskName}-cpuunit"

#ECSTaskMemory
  ECSTaskMemory:
    Value: !Ref ECSTaskMemory
    Export:
      Name: !Sub "${PJPrefix}-${ECSTaskName}-memory"

#ECSContainer
  ECSContainerName:
    Value: !Sub "${PJPrefix}-${ECSContainerName}"
    Export:
      Name: !Sub "${PJPrefix}-${ECSContainerName}-name"

#ECSImage
  ECSImageName:
    Value: !Ref ECSImageName
    Export:
      Name: !Sub "${ECSImageName}-name"

#ECSServiceName
  ECSService:
    Value: !Sub "${PJPrefix}-${ECSServiceName}"
    Export:
      Name: !Sub "${PJPrefix}-${ECSServiceName}-name"

#ECSServiceARN
  ECSServiceARN:
    Value: !Ref ECSService
    Export:
      Name: !Sub "${PJPrefix}-${ECSServiceName}-arn"

#ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Value: !Ref ECSTaskDesiredCount
    Export:
      Name: !Sub "${PJPrefix}-${ECSTaskName}-count"
```