簡単なインフラ環境をAWS Cloud Formationを使って構築してみた

# はじめに
AWSで環境構築したいけど、マネジメントコンソールぽちぽちは手順書の作成めんどくさいよね、ということで、CloudFormationを使用して、シンプルな構成のシステムを自動で構築してみました。
YAML（もしくはJSON）でテンプレートを作成するだけで、マネジメントコンソールをぽちぽちしなくても自動で環境を作ってくれて超便利です！

# 環境の自動化がなぜ重要か
- 環境構築での人為的ミスを減らせる→信頼性の向上
- 開発速度を上げることができる
- DevOps（開発と運用の一体化）とCI/CD（継続的デリバリー）での開発を実現できる

## AWSにおける環境を自動化するための主なサービス
- Codeシリーズ（Git上のコードのコミット・実行・デプロイの自動化）
- Cloud Formation
- ECS（Dockerコンテナによる環境構築の自動化）
- Elastic Beanstalk(Webアプリケーションのデプロイ自動化)
- OpsWorks(サーバの設定、デプロイ、管理の自動化)  
など

今回は、Cloud Formationでの環境構築の自動化をやっていきます。

# Cloud Formationとは
- インフラストラクチャリソースのテンプレートを利用して自動で構築することができる環境自動設定サービス
- AWS内の全てのインフラリソースをテンプレート化することができる
- テンプレートはJSONかYAMLで記述する
- Code Piplineと組み合わせることで、テンプレートの変更、実行、展開を自動化できる
- Cloud Formationデザイナーを使うことで、視覚的にテンプレートを作成することも可能

## Cloud Formationを使う利点
- 環境構築を効率化できる
- いつでも同じ設定の環境を構築することができる（標準化）
- ソフトウェアと同じように構成管理ができる

## テンプレートについて
AWS公式ドキュメントの[テンプレートリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-reference.html)に全て情報が載っているので、こちらを見ながら作成していきます。

```sample.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: A sample template
Metadata:
Parameters:
Mappings:
Conditions:
Resources:
  MyEC2Instance:
    Type: AWS::EC2::Instance
# 省略
```
- AWSTemplateFormatVersion: 必ず頭に記述する
- Description: テンプレートの説明
- Metadata: メタデータ
- Parameters: 共通で使いたい要素を参照値として記述
- Mappings: Parametersの値が複数バージョン
- Conditions: リソース作成時の条件内容を記述
- Resources: 実際に作りたいリソースの内容を記述

# Cloud Formationで環境構築をしてみる
## どんな環境を作るか
以下の構成の環境を、Cloud Formationを使って構築していきます。
![CloudFormation.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251142/af33b720-e0b5-b5cd-7f1a-ac14555d5b5c.jpeg)


## VPCの作成
### YAMLファイルの記述
```template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: A sample template
Parameters:
Resources:
  MyVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: '10.10.0.0/16'
      Tags:
        - Key: 'Name'
          Value: 'sample-vpc'
```
- MyVPC: 任意のリソース名
- Type: 作成するリソースを記載。VPCのリソース名は、"AWS::EC2::VPC"
- Properties: プロパティ
- CiderBlock: 任意のCIDRブロックを設定する。"10.10.0.0/16"を設定することで、このVPC内では10.10.0.0~10.10.255.255までのIPアドレスが使用可能になる。
- Tags: タグをつけるときに使用する

## サブネットの作成
パブリックサブネットを1つ、プライベートサブネットを2つ作成します。
### YAMLファイルの記述
```template.yaml
# 省略

Resources:
　 # 省略
  MyPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.11.0/24'
      MapPublicIpOnLaunch: true
      Tags:
        - Key: 'Name'
          Value: 'sample-public-subnet'
  MyPrivateSubnet1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.21.0/24'
      AvailabilityZone: 'ap-northeast-1a'
      Tags:
        - Key: 'Name'
          Value: 'sample-private-subnet1a' 
  MyPrivateSubnet1C:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.22.0/24'
      AvailabilityZone: 'ap-northeast-1c'
      Tags:
        - Key: 'Name'
          Value: 'sample-private-subnet1c'   
```
- VpcId: このサブネットが所属するVPCのIDもしくはリソース名を記述する
    - Ref関数: 指定したパラメータまたはリソースの値を返す（ここではMyVpcを返す）
- MapPublicIpOnLaunch: このサブネットで起動されたインスタンスがパブリックIPアドレスを受け取る場合はtrueにする（デフォルトはfalse）

## インターネットゲートウェイの作成
### YAMLファイルの記述
```template.yaml
# 省略

Resources:
　 # 省略
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: 'Name'
        Value: 'sample-igw'
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVpc
      InternetGatewayId: !Ref MyInternetGateway  
```
AttachGatewayでVPCにインターネットゲートウェイをアタッチする

## パブリックサブネットの作成
インターネットゲートウェイとパブリックサブネットをルートテーブルで紐付ける
### YAMLファイルの記述
```template.yaml
# 省略

Resources:
　 # 省略
  MyRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: 'Name'
          Value: 'sample-rt'
      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 MyPublicSubnet
      RouteTableId: !Ref MyRouteTable
```
インターネットゲートウェイを指定したルート（MyRoute）をルートテーブル（MyRouteTable）に設定し、ルートテーブルをパブリックサブネットにMySubnetRouteTableAssociationで紐付けた

## ここまでできました
![CloudFormation.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251142/867e05ac-c78b-73bb-7826-86bd1078e1f9.jpeg)


## EC2インスタンスを作成
パブリックサブネットにEC2インスタンスを設置する
### YAMLファイルの記述
```template.yaml
# 省略
Parameters:
  KeyPair:
    Description: Select KeyPair Name.
    Type: AWS::EC2::KeyPair::KeyName

Resources:
　 # 省略
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: 'ami-xxxxxx' # xにはidがはいる
      InstanceType: t2.micro
      SubnetId: !Ref MyPublicSubnet
      BlockDeviceMappings:
        - DeviceName: '/dev/xvda'
          Ebs:
            VolumeType: 'gp2'
            VolumeSize: 8
      Tags:
        - Key: 'Name'
          Value: 'sample-ec2-instance'
      SecurityGroupIds:
        - !Ref MyEC2SecurityGroup
      KeyName: !Ref KeyPair
  MyEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref MyVpc
      Tags:
        - Key: 'Name'
          Value: 'sample-ssh-sg'
      GroupDescription: Allow ssh
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '22'
        ToPort: '22'
        CidrIp: 0.0.0.0/0
```
- ImageId: 使用するAMIのIDを記述する
- SubnetId: EC2インスタンスを設置するサブネットを指定
- BlockDeviceMappings: インスタンスにアタッチするブロックデバイスを定義
- KeyName: KeyPairはParameterで設定しており、マネジメントコンソールで指定する
- SecurityGroupIngress: 受信規則を指定

## DBインスタンスの作成
プライベートサブネットにDBインスタンスを設置する。RDSを使用する。マルチAZ構成にする。

### YAMLファイルの記述
```template.yaml
# 省略
Parameters: 
  # 省略
  MyRDSMasterUser:
    Type: String
    Default: admin
    MinLength: 1
    MaxLength: 16
    NoEcho: true
    AllowedPattern: '[a-z]+'
  MyRDSMasterPassword:
    Type: String
    Default: password
    MinLength: 8
    MaxLength: 16
    NoEcho: true
    AllowedPattern : '[^\/@"]+'

Resources:
　 # 省略
  MyRDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: SubnetGroup for RDS
      SubnetIds:
        - !Ref MyPrivateSubnet1A
        - !Ref MyPrivateSubnet1C
  MyRDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SecurityGroup for DB
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: '3306'
          ToPort: '3306'
      VpcId: !Ref MyVpc
  MyRDSDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: '5'
      DBSubnetGroupName: !Ref MyRDSSubnetGroup
      VPCSecurityGroups: 
      - !Ref MyRDSSecurityGroup
      MultiAZ: true
      DBInstanceClass: db.t2.micro
      DBName: 'sample'
      Engine: MySQL
      EngineVersion: 5.7.22
      MasterUsername: !Ref MyRDSMasterUser
      MasterUserPassword: !Ref MyRDSMasterPassword
    DeletionPolicy: Snapshot
```
- MyRDSMasterUser(Parameter): DBのユーザ名のバリデーションを設定（ユーザ名はマネジメントコンソールで設定する）
- MyRDSMasterPassword(Parameter): DBのパスワードのバリデーションを設定（パスワードはマネジメントコンソールで設定する）
- MultiAZ: マルチAZ構成にする場合はtrue（デフォルトはfalse）
- Engine: DBエンジンを指定（今回はMySQL）
- MasterUsername: マネジメントコンソールで設定したMyRDSMasterUserをユーザ名として使用
- MasterUserPassword: マネジメントコンソールで設定したMyRDSMasterPasswordをパスワードとして使用
- ハマりポイント: VPCSecurityGroupsは配列での指定じゃないとダメでした

## 完成！
![CloudFormation.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251142/7430da01-39aa-4bb3-06d6-9e14c042f338.jpeg)

## 完成したテンプレート
```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: A sample template
Parameters:
  KeyPair:
    Description: Select KeyPair Name.
    Type: AWS::EC2::KeyPair::KeyName
  MyRDSMasterUser:
    Type: String
    Default: admin
    MinLength: 1
    MaxLength: 16
    NoEcho: true
    AllowedPattern: '[a-z]+'
  MyRDSMasterPassword :
    Type: String
    Default: password
    MinLength: 8
    MaxLength: 16
    NoEcho: true
    AllowedPattern : '[^\/@"]+'

Resources:
  MyVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: '10.10.0.0/16'
      Tags:
        - Key: 'Name'
          Value: 'sample-vpc'
  MyPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.11.0/24'
      MapPublicIpOnLaunch: true
      Tags:
        - Key: 'Name'
          Value: 'sample-public-subnet'
  MyPrivateSubnet1A:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.21.0/24'
      AvailabilityZone: 'ap-northeast-1a'
      Tags:
        - Key: 'Name'
          Value: 'sample-private-subnet1a' 
  MyPrivateSubnet1C:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVpc
      CidrBlock: '10.10.22.0/24'
      AvailabilityZone: 'ap-northeast-1c'
      Tags:
        - Key: 'Name'
          Value: 'sample-private-subnet1c' 
  MyInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: 'Name'
        Value: 'sample-igw'
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVpc
      InternetGatewayId: !Ref MyInternetGateway
  MyRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: 'Name'
          Value: 'sample-rt'
      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 MyPublicSubnet
      RouteTableId: !Ref MyRouteTable

  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: 'ami-xxxxxx' # xにはidがはいる
      InstanceType: t2.micro
      SubnetId: !Ref MyPublicSubnet
      BlockDeviceMappings:
        - DeviceName: '/dev/xvda'
          Ebs:
            VolumeType: 'gp2'
            VolumeSize: 8
      Tags:
        - Key: 'Name'
          Value: 'sample-ec2-instance'
      SecurityGroupIds:
        - !Ref MyEC2SecurityGroup
      KeyName: !Ref KeyPair
  MyEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref MyVpc
      Tags:
        - Key: 'Name'
          Value: 'sample-ssh-sg'
      GroupDescription: Allow ssh
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0

  MyRDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: SubnetGroup for RDS
      SubnetIds:
        - !Ref MyPrivateSubnet1A
        - !Ref MyPrivateSubnet1C
  MyRDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SecurityGroup for DB
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: '3306'
          ToPort: '3306'
      VpcId: !Ref MyVpc
  MyRDSDBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: '5'
      DBSubnetGroupName: !Ref MyRDSSubnetGroup
      VPCSecurityGroups: 
      - !Ref MyRDSSecurityGroup
      MultiAZ: true
      DBInstanceClass: db.t2.micro
      DBName: 'sample'
      Engine: MySQL
      EngineVersion: 5.7.22
      MasterUsername: !Ref MyRDSMasterUser
      MasterUserPassword: !Ref MyRDSMasterPassword
    DeletionPolicy: Snapshot
```

# 参考資料
- [AWS公式ドキュメント（テンプレートリファレンス）](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-reference.html)
- [これだけでOK！ AWS 認定ソリューションアーキテクト – アソシエイト試験突破講座（初心者向け21時間完全コース）](https://www.udemy.com/aws-associate/)