【CloudFormationハンズオン】勉強したことのまとめ

## はじめに  
 突然ですがCloudFormationを今まで体系的に勉強したことがなかったので、お盆休みを使ってまとめてみました。
## 目次  
- 前提条件  
- CloudFormationとは？
- CloudFormation基礎用語
- とりあえず動かしてみよう
- パラメーターを定義してスタックを作成しよう
- Mappingsを使って汎用性の高いテンプレートを作成しよう 
- 組み込み関数について理解しよう
- 本格的なインフラを構築しよう
- おわりに

## 前提条件  
## CloudFormationとは？  
CloudFormationはAWSリソースの構成管理ツールです。jsonやymlといったテキスト形式でAWSリソースを宣言的に記述することで、AWS上でのインフラストラクチャの構築・削除を自動化することができます。AWSリソースをコードで記述しておくことで、インフラのアーキテクチャを簡単に把握することができます。例えば、開発チームに新人が入ってきたとしてもCloudFormationのテンプレートを見れば容易にアーキテクチャの概要を知ることができます。また、CloudFormationでインフラを構築していれば、既存の環境を変更することができ、その変更箇所も容易に把握することができます。私個人のユースケースとしては、検証用途に作った環境をいつでも再現できるようにするために利用しています。会社の人に「あの環境すぐ作れる？」って聞かれたら「秒でつくれます」とドヤれます。めっちゃ便利です。  

## CloudFormation基礎用語  
### テンプレート  
EC2やVPCなどのAWSリソースが定義されたテキストファイルのことです。このファイルに作成したいリソースやリソースの設定など記述します。CloudFormationではjsonとyamlをサポートしています。(この記事ではymlを使用します。というか特別な理由がなければyamlを使うことをお勧めします。)
### スタック  
テンプレートから作成されたAWSリソース群のことです。テンプレートと何が違うの？とツッコミをいれそうになった人は、テンプレートはテキストファイルそのもの、スタックはAWSリソース、くらいの認識で大丈夫かと思います。  

CloudFormationの大枠を掴むにはこの二つを理解していれば大丈夫です。次からは実際にテンプレートを記述していきましょう。


## とりあえず動かしてみよう  
### スタックの作成
何はともあれ、まずは実際に手を動かしてスタックを作成してみましょう。以下の内容が書かれたymlファイルを作成してください。  

```:hello.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template
Resources:
  MyFirstInstance:　#任意のリソース名
    Type: "AWS::EC2::Instance"
    Properties:　#EC2インスタンスの詳細設定
      ImageId: "ami-0c3fd0f5d33134a76"
      InstanceType: t2.micro
      Tags:
        - Key: "Name"
          Value: "test-ec2"
      KeyName: "mykey"  #EC2ダッシュボードから任意のキーペアを作成してください。
      BlockDeviceMappings:
        - 
          DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
```
CloudFormationのダッシュボードから【スタックの作成】>【テンプレートファイルのアップロード】>【ファイルのアップロード】を選択し、先ほど作成したテンプレートをアップロードしましょう。【次へ】を選択して任意のスタック名を入力してください。次のオプションのスタック設定ではデフォルトのままにしておき、最後に【スタックの作成】を選択します。数秒後、EC2ダッシュボードに移動してみるとEC2インスタンスが作成されていることが確認できます。
![スクリーンショット 2019-08-10 13.01.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/d101867c-5b86-8443-b2cc-6ab5ec5aaeff.png)　　

### スタックの変更  
次にスタックを変更してみましょう。インスタイプをt2.microからt2.nanoに変更します。

```
AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template
Resources:
  MyFirstInstance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: "ami-0c3fd0f5d33134a76"
      InstanceType: t2.nano  #t2.nanoに変更
      Tags:
        - Key: "Name"
          Value: "test-ec2"
      KeyName: "mykey"  
      BlockDeviceMappings:
        - 
          DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
```
先ほどのスタックの詳細画面から【スタックの変更】を選択して新しいテンプレートを再アップロードしてください。【スタックの更新】を選択し、数秒後EC2ダッシュボードから確認するとインスタンスサイズがt2.microからt2.nanoに変更されています。 
![スクリーンショット 2019-08-10 13.14.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/2e0d838f-0805-c588-68f9-6c5046e2bf76.png)

### スタックの削除  
最後にリソースのクリーンアップを行いましょう。スタックの詳細画面から【スタックの削除】を選択し、削除してください。  

ここまでやってみていかがだったでしょうか？作成したリソースがEC2インスタンスだけなので、まだCloudformationの恩恵を感じにくいかもしれません。最後のセクションでもう少し複雑な構成のテンプレートをつくっていきます。

## Parametersを定義してスタックを作成しよう  
次に[Parameters](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)を使ってテンプレートを作成していきましょう。Parametersとはプログラミングで言う所の変数のようなものでテンプレート内であらかじめ宣言しておくことでスタックの作成時に宣言したParametersの中から動的に値を選択することがきます。以下の内容を見てください。  

```:parameters.yml

AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template

Parameters:  #Parametersの定義
  InstanceParameter:
    Type: String
    Default: t2.micro　#何も選択しなかった場合の選択肢
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.nano
    Description: select instancetype #コンソールでの操作時にこの内容が表示される

Resources:
  MyFirstInstance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: "ami-0c3fd0f5d33134a76"
      InstanceType: !Ref InstanceParameter  #Parametersで定義した値を参照
      Tags:
        - Key: "Name"
          Value: "test-ec2"
      KeyName: "mykey"  
      BlockDeviceMappings:
        - 
          DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
```
Parametersのセクションで複数のインスタンスタイプを宣言しています。こうすることによって、開発環境や本番環境でインスタンスタイプを変更したい場合などにわざわざ別のテンプレート作成せずに同じテンプレートを使い回すことができます。では実際にスタックを作成していきます。
ファイルをアップロードして次のような画面が出てくれば成功です。ここで何も選択しなかった場合はデフォルト値として定義したt2.microでインスタンスが作成されます。
![スクリーンショット 2019-08-10 16.27.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/cb649cce-e1f5-c64f-7d00-965f3318c71d.png)

## Mappingsを使って汎用性の高いテンプレートを作成しよう  
次に[Mappings](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html)を使ってテンプレートを作成していきます。AWSをかじったことのある方ならご存知かもしれませんが、EC2インスタンスのAMI-IDはリージョンによって異なっています。リージョンごとに適切なAMI-IDが記述されたテンプレートを用意するのは面倒です。そこでMappingsの出番です。Mappingsセクションにキーバリュー形式で値を宣言し、FindInMap関数で参照することができます。実際のテンプレートを見てみましょう。この例では東京リージョンとバージニアリージョンでスタックを作成することを想定しています。このテンプレートを実行すると東京リージョンにはAmazon Linux2、バージニアにはUbuntuのインスタンスが作成されます。

```:mappings.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template

Mappings: 
  RegionMap: 
    us-east-1: 
      hvm: "ami-07d0cf3af28718ef8" # 東京リージョンではAmazon Linux
    ap-northeast-1: 
      hvm: "ami-0c3fd0f5d33134a76" # バージニアリージョンではUbuntu

Resources:
  MyFirstInstance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", hvm] #FindInMap関数でリージョンの参照。このテンプレートがどのリージョンで実行されるかによって作成される関数が変わってくる。
      InstanceType: t2.micro
      Tags:
        - Key: "Name"
          Value: "test-ec2"
      KeyName: "mykey"  
      BlockDeviceMappings:
        - 
          DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
```
EC2のダッシュボードで確認すると、リージョンによって、OSが異なっていますね。 
 
- 東京リージョン  

![スクリーンショット 2019-08-10 17.38.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/691c6cca-36b6-e5a2-54c4-5e7175321e79.png)

- バージニア北部リージョン  

![スクリーンショット 2019-08-10 17.39.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/411ad9a6-1fb1-ea31-9609-5e9afe9deca4.png)


## 組み込み関数を理解しよう  
次に[組み込み関数](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)を使ってテンプレートを作成していきましょう...と言いたいところなのですが、実はもう組み込み関数使ってます。RefとFindInMapってやつですね。RefはParametersを参照するときに使って、FindInMapはMappingsを参照するときに使います。個人的にこの２つを使いこなせれば問題ないと思うのですが、気になる方は[公式ドキュメント]()を参照してみるといいかもです。(手抜きしてすいません...)

## 本格的なインフラを構築しよう  
ここまでひたすらEC2インスタンスを作成してきましたが、セキュリティグループも定義してないし、VPCもでフォルダだしこれだと検証用途にも使えないと思います。なので、このセクションではこれまでやってきたことを使って、もう少し本格的なインフラを構築していきます。インフラの概要を図で見ていきましょう。VPCの中にサブネットを作成し、その中に2つのEC2インスタンスを作成しています。これまで作成したEC2はセキュリティグループが設定されておらず、SSH接続ができませんでしたが、このテンプレートではセキュリティグループを作成し、0.0.0.0/0つまり全てのホストからのアクセスを許可しています。これで秘密鍵を持っているクライアントはこのEC2インスタンスにアクセスできるようになります。スタックを作成したらテンプレートの設定が反映されているか確認してみてください。

<img width="553" alt="スクリーンショット 2019-08-10 19.51.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286861/0b17df50-4d0f-3139-14db-bf6b6e1bc00e.png">


```:infra.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template

Parameters:  
  FirstInstanceParameter: #1つ目のインスタンス用のパラメーター
    Type: String
    Default: t2.micro 
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.nano
    Description: select instancetype 

  SecondInstanceParameter: #2つ目のインスタンス用のパラメーター
    Type: String
    Default: t2.micro 
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.nano
    Description: select instancetype 

Mappings: 
  RegionMap: 
    us-east-1: 
      hvm: "ami-07d0cf3af28718ef8" # 東京リージョンではAmazon Linux
    ap-northeast-1: 
      hvm: "ami-0c3fd0f5d33134a76" # バージニアリージョンではUbuntu

Resources:
# ネットワーク系のリソース
  MyVpc:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: '10.0.0.1/16'
      Tags:
        - Key: 'Name'
          Value: 'MyVpc'
  MySubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock: '10.0.10.0/24'
      MapPublicIpOnLaunch: true
      Tags:
        - Key: 'Name'
          Value: 'MySubnet'
      VpcId: !Ref MyVpc
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: 'Name'
        Value: 'MyIGW'
  MyAttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVpc
      InternetGatewayId: !Ref MyInternetGateway
  MyRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: 'Name'
          Value: 'MyRoute'
      VpcId: !Ref MyVpc
  MyRoute:
    Type: AWS::EC2::Route
    DependsOn: MyInternetGateway
    Properties:
      RouteTableId: !Ref MyRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref MyInternetGateway
  MySubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref MySubnet
      RouteTableId: !Ref MyRouteTable
# EC2インスタンス群
  MyFirstInstance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", hvm]
      InstanceType: !Ref FirstInstanceParameter 
      Tags:
        - Key: "Name"
          Value: "first-test-ec2"
      KeyName: "mykey"  
      SubnetId: !Ref MySubnet
      SecurityGroupIds:
        - !Ref MySecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
  MySecondInstance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", hvm]
      InstanceType: !Ref SecondInstanceParameter 
      Tags:
        - Key: "Name"
          Value: "second-test-ec2"
      KeyName: "mykey"  
      SubnetId: !Ref MySubnet
      SecurityGroupIds:
        - !Ref MySecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/sdm
          Ebs:
            VolumeType: io1
            Iops: 200
            DeleteOnTermination: false
            VolumeSize: 20
  # セキュリティグループ
  MySecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "MySecurityGroup"
      VpcId: !Ref MyVpc
      Tags:
        - Key: 'Name'
          Value: 'MySG'
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '22'
        ToPort: '22'
        CidrIp: 0.0.0.0/0
```

## おわりに  
CloudFormationを使う前まではTerraform信者だったんですが、AWSで使うとなるとやっぱりCloudFormation一強ですね。最後に作成したテンプレートはネットワーク系のリソースとEC2のリソースをひとまとめにしたのですが、インフラの規模が大きくなると可読性が落ちるのでリソースの種類によってテンプレートを分けると良さそうです。
参考資料：[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)