CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(前編)

# 前提条件
「CFn？IaC？何それ美味しいの？」な超初心者向け。

とは言え、AWSの機能を使おうって言うんだから、[AWS BlackBeltオンラインセミナーのCloudFormation編](https://www.youtube.com/watch?v=HU47ZAM3mtw)くらいは見ていると読みやすいと思う。

あと、CFnは知らなくても同じことをマネジメントコンソールでポチポチできるくらいの理解度は前提。

文中後半で動かしているアプリは、8080ポートで待ち受けるHTTPサーバ。
SpringBootなWebアプリと考えてもらえれば。

記事中には、備忘のためにリファレンスに書かれていないデフォルト値を整理しておくが、2020年3月時点の情報であり、後でAWSが仕様を変えたとしても追従する予定はないので、挙動が違ったらリファレンスを見直してほしい。あと、今回の構成(ECS on FargateのBlue/Greenデプロイメント)以外の構成以外のデフォルト値まで調査はしていないのであしからず。

# 環境を整える
1から理解しながら書くので、力技でいく。
基本の流れは、「リファレンス見ながら書く→構文チェック→動かしてみる→修正する→…」の繰り返し。

というわけで、以下の記事を参考に構文チェッカをインストールしておく。

【Qiita】[cfn-python-lint](https://qiita.com/komeda-shinji/items/f0d106481313340438bf)

pipのインストールは以下の記事を見ながら。

【AWS公式】[Linux で Python、pip、EB CLI をインストールする](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/eb-cli3-install-linux.html)

cfn-lintは、↓こんな感じで間違っている箇所を教えてくれる。便利！

```shell
$ cfn-lint createALB.yml
E0000 Null value at line 13 column 22
createALB.yml:13:22
```

# とりあえず空っぽのALBを作ってみる
## スタックの作成
まずは、ALBを作ってみる。最初はEC2じゃないのかよ！というツッコミがあるのは重々承知しているが、とりあえずALBで。

こういうことをやっているサイトは探してみれば色々とあるものの、結局最後に頼るのは[AWS公式のユーザーガイド](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)のリファレンスなのである。これを見ながらしっかり理解していく。
リファレンスで ```AWS::ElasticLoadBalancingV2::LoadBalancer``` を選択して、プロパティを設定していく。「必須: いいえ」になっているプロパティがたくさんあるので、そういったものはとりあえずザクザク削っていってみよう。削った場合のデフォルト値が書いてないから不安になるけど、気にしない。

```YAML:createALB.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  ALB Create

Resources:
  # ------------------------------------------------------------#
  #  ALB
  # ------------------------------------------------------------#
  ALB: 
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      Name: CFn-test-ALB
      Subnets: 
        - [自分のサブネットID①]
        - [自分のサブネットID②]
        - [自分のサブネットID③]
```

えっ、こんなにシンプルでいいの？と思いつつ、マネコンのCloudFormationの「スタック」で「スタックの作成」ボタンを押す。選択肢が出るので「新しいリソースを使用(標準)」を選択する。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f08f7262-27d3-a835-41d3-57a0821c7911.png)

こんな感じでYAMLのファイルをアップロードをする。
あとは、「次へ」で進みまくる。細かい設定は今は気にしなくてOK。スタックの名前は「CFn-test」とか適当につけておく。

流し終わると↓こんな感じになる。

![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/54cf67ab-bb51-9413-8f1d-dbc09b398dc2.png)

ちゃんとCREATE_COMPLETEされているので、ALBを見に行くと、

![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/8ad66833-94dd-009b-42ef-10aa3fa59d09.png)

ちゃんとできてる！すごい！

ちなみに、省略したプロパティのデフォルト値でリファレンスに書いていない値は、ALBの場合は↓こんな感じになるようである。「ALBの場合は」と書いてあるのは、NLBの場合はデフォルト値が違う可能性があるから。試していないので分からない。
リファレンスに書いておいてほしい…。

| プロパティ | デフォルト値 |
|:--|:--|
| IpAddressType | ipv4 |
| LoadBalancerAttributes | なし？ |
| SecurityGroups | default VPC security group |
| SubnetMappings/Subnets | 省略不可。省略すると cfn-lint で ```E2523 Only one of [SubnetMappings, Subnets] should be specified for Resources/ALB/Properties``` のエラーになる |
| Tags | なし |

## 作成したスタックを修正する
さて、作られたALBを見てみると、セキュリティグループがデフォルトになっている。
さすがにこれはちょっとセキュリティ的にアレなので、テンプレートのYAMLのALBのプロパティに以下を追加する。

```YAML
      SecurityGroups: 
        - [セキュリティグループID]
```

セキュリティグループは、新規で作るか、既存のを持ってくるか。
今回は、インバウンドの80, 8080ポートを開放している既存のセキュリティグループをアタッチする。

テンプレートを修正したら、スタックの詳細画面で「更新」を押して、テンプレートを差し替えていく。
ステータスが UPDATE_COMPLETE になったら、もう一度ALBの設定を見てみると、

![キャプチャ4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/88ee928e-b11b-add3-d1e2-264870421d5b.png)

無事、設定が反映されている。

## 作成したスタックを削除する
これも簡単で、スタックの詳細画面から「削除」すれば良い。
CloudFormationで作成したスタックのリソースは、スタックの削除に紐付けてまとめて掃除してくれるらしい。便利！

ちなみに、マネジメントコンソールから先回りしてALBを削除をしておいたらどうなるか実験したが、問題なく DELETE_COMPLETE のステータスになった。最終結果が合えば良いらしい。

# ALBのリスナー・ターゲットグループを設定する
このセクションでは、空っぽのALBを完成品にして、80ポートで待ち受けしているEC2インスタンスにフォワードするところまで。待ち受けするEC2インスタンスを立てる手順をここで書くのは本筋とは違うので、分からなければ[この記事](https://qiita.com/neruneruo/items/ff6747a4129a5b52e6c6)で ```docker run -p 80:8080 ...``` しているあたりまでやっておく。テンプレートの中で作ってもいいけど、それも面倒なので今回は有り物を使うということで。

今回の完成品のテンプレートのYAMLは以下。ALBのリソース部分は前回と同じ。

```YAML:createALB_TG.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  ALB Create

Resources:
  # ------------------------------------------------------------#
  #  ALB
  # ------------------------------------------------------------#
  ALB: 
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      Name: CFn-test-ALB
      SecurityGroups: 
        - [セキュリティグループID]
      Subnets: 
        - [自分のサブネットID①]
        - [自分のサブネットID②]
        - [自分のサブネットID③]
  ALBLISTENER: 
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref ALBTG
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
  ALBTG:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: 
      Name: CFn-test-ALB-TG
      Targets: 
        - Id: [EC2インスタンスID]
          Port: 80
      Port: 80
      Protocol: HTTP
      VpcId: [VPCID]
```

## ALBのターゲットグループ
ALBのリソースとほぼ同じような記述方法なので、特に目新しい部分は無いと思う。

ターゲットグループのプロパティで、リファレンスにALBの場合のデフォルト値が明記されていないものは以下。
なお、```Port``` と ``` Protocol``` はリファレンスで「必須: いいえ」と書いてあったし cfn-lint でも引っかからなかったので指定をしなかったら、CREATE の際にエラーになったので指定した。

| プロパティ | デフォルト値 |
|:--|:--|
| Matcher | HttpCode: "200" |
| Name | AWS払い出しの名前 |
| TargetGroupAttributes | それぞれの属性のプロパティのデフォルト値(リファレンスに書いてある) |
| Targets | なし |

## ALBのリスナー
ここはデフォルト値がリファレンスに明記されているので分かりやすかった。

ポイントは以下の部分。

```YAML
      DefaultActions: 
        - TargetGroupArn: !Ref ALBTG
          Type: forward
      LoadBalancerArn: !Ref ALB
```

リスナーの定義には、ALBのARNとターゲットグループのARNが必須パラメータになっているが、どちらもこのテンプレート内でリソースを作成しているので、あらかじめ作っておいたリソースのARNをマネジメントコンソールから転記することができない。なので、同一テンプレート内で作成したリソースのARNを参照するためのCloudFormationの機能を使うのである。
※そういう意味では、ターゲットグループのVPCやインスタンスなんかも、同じテンプレート内でリソース定義してRefすれば全部ひっくるめてコード化できるけど、今回はやらなかった。

各リソースに対して ```!Ref``` した場合に何が取得できるかは、ユーザーガイドの各リファレンスの「戻り値」に 記載されている。

# ECSでサービスを起動する
## ECSで起動するためのALBの設定の見直し
最終的にBlue/GreenデプロイメントできるECSの起動を行いたいので、ALBはBlue用とGreen用の2つの待ち受けをできるようにする。つまり、ターゲットとリスナーをそれぞれ2つ定義すれば良い。
また、Blue/Greenデプロイメント環境にするのであれば、ターゲットグループのタイプ指定を ```ip```  にしておく必要がある。

Outputsは、テンプレートファイルを分けるなら必要。
ALB作成のテンプレートのアウトプットをエクスポートして、次のECS作成してインポートすることが可能。

```YAML
  ALBLISTENER1: 
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref ALBTG1
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
  ALBLISTENER2: 
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref ALBTG2
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 8080
      Protocol: HTTP
  ALBTG1:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: 
      Name: CFn-test-ALB-TG1
      Port: 80
      Protocol: HTTP
      TargetType: ip
      VpcId: [VPCID]
  ALBTG2:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties: 
      Name: CFn-test-ALB-TG2
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      VpcId: [VPCID]

Outputs:
  TargetGroupArn1:
    Description: ALB Target Group Arn 1
    Value: !Ref ALBTG1
    Export:
      Name: !Sub ${AWS::StackName}-TargetGroupArn1
  TargetGroupArn2:
    Description: ALB Target Group Arn 2
    Value: !Ref ALBTG2
    Export:
      Name: !Sub ${AWS::StackName}-TargetGroupArn2
```

## ECSクラスターの作成
これは悩むことはないはず。適当に名前を付ければOK。

```YAML
  ECSCLUSTER:
    Type: AWS::ECS::Cluster
    Properties: 
      ClusterName: CFn-test-Cluster
```

## ロググループの作成
最悪無くても良いのだが、CloudWatch Logsにコンテナのログを出力しておかないと、何かあった時に手も足も出なくなってしまうので、新規のロググループをリソースに追加しておくことをオススメする。

```YAML
  LOGGROUP:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /ecs/CFn-test-Container
```

## タスク定義
ここはプロパティが大量。
今回は Fargate でコンテナ起動をすることを前提にしているので、Fargate対象外のプロパティのデフォルト値調査は割愛。

| プロパティ | デフォルト値 |
|:--|:--|
| IpcMode | よく分からない… |
| NetworkMode | host |
| ProxyConfiguration | AppMeshのプロキシ設定無効 |
| Tags | 無し |

タスク定義の中のコンテナ定義( ```ContainerDefinitions``` )も色々と設定項目がある。
ただ、ここはそもそものコンテナ定義の理解が必要なのと、その理解にはかなり時間がかかりそうなので、今回は必要な設定のみを考える。

| プロパティ | デフォルト値 |
|:--|:--|
| Family| AWS払い出しの名前 |

最終的に、タスク定義のリソースについては↓こんなイメージ。
ARNの指定については、```!Sub ${AWS::AccountId}``` や ```!Sub ${AWS::Region}```などを使ってなるべくベタ書きをしないようにした方が良い。
LogConfiguration は↑で作成したロググループをコンテナに紐付けるプロパティ。ログ出力しないのであれば不要。

```YAML
  ECSTASKDEFINITION:
    Type: AWS::ECS::TaskDefinition
    Properties: 
      RequiresCompatibilities: 
        - FARGATE
      Cpu: "256"
      ExecutionRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole
      Family: CFn-test-task
      Memory: "512"
      NetworkMode: awsvpc
      TaskRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole
      ContainerDefinitions: 
        - Name: CFn-test-Container
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/my-greeting-web:52c324b
          Memory: 512
          PortMappings: 
            - ContainerPort: 8080
              HostPort: 8080
              Protocol: HTTP
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LOGGROUP
              awslogs-region: !Sub ${AWS::Region}
              awslogs-stream-prefix: ecs
```

## ECSサービスの作成
ここもプロパティがたくさんあって大変。

これ、死ぬほど悩んだのだが、日本語ドキュメントで翻訳されていない  ```DeploymentController``` というプロパティがあって、これがマネジメントコンソール上でサービスを作成するときの「デプロイメントタイプ」に該当する。デフォルトがECSなので、Blue/Greenデプロイメントができないように思えてしまうけど、実際には指定したら作れた。

| プロパティ | デフォルト値 |
|:--|:--|
| DeploymentConfiguration | MinimumHealthyPercent: 100, MaximumPercent: 200 |
| DeploymentController | ECS |
| EnableECSManagedTags | よく分からない… |
| HealthCheckGracePeriodSeconds | 0 |
| LaunchType | EC2 |
| LoadBalancers | 無し |
| PlacementConstraints | 制約無し |
| PlacementStrategies | 戦略無し |
| PropagateTags | よく分からない… |
| SchedulingStrategy | REPLICA |
| ServiceName | AWS払い出しの名前 |
| ServiceRegistries | 無し |
| Tags | 無し |

サービスの設定については最終的に以下のような感じになる。
ここで、ALBの設定のときにエクスポートした値を ```!ImportValue CFn-test-TargetGroupArn1``` して取り込んでいる。

```YAML
  ECSSERVICE:
    Type: AWS::ECS::Service
    Properties: 
      Cluster: !Ref ECSCLUSTER
      ServiceName: CFn-test-ECSService
      LaunchType: FARGATE
      DesiredCount: 1
      DeploymentController:
        Type: CODE_DEPLOY
      LoadBalancers: 
        - ContainerName: CFn-test-Container
          ContainerPort: 8080
          TargetGroupArn: !ImportValue CFn-test-TargetGroupArn1
      NetworkConfiguration: 
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups: 
            - [セキュリティグループID]
          Subnets: 
            - [自分のサブネットID①]
            - [自分のサブネットID②]
            - [自分のサブネットID③]
      TaskDefinition: !Ref ECSTASKDEFINITION
```

ここまでやると、Blue/GreenデプロイメントするためのECS on Fargateが1アクションないし2アクションで起動する。

さて、ここからCI/CDパイプラインまで自動構築してみよう！というところで[中編](https://qiita.com/neruneruo/items/d62140f29f067b28925b)に続く。