AWS CloudFormationのスタックを分割してインフラのスクラップアンドビルドをやりやすくする

# AWS CloudFormationについて
AWS の CloudFormation は、インフラに必要なリソース（VPC や EC2 など）をテンプレートに定義して実行するだけで作成や削除をまとめてやってくれる、AWS専用の Infrastructure As Code 環境です。

- [ユーザーガイド: AWS CloudFormation とは](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)

オンプレミスのインフラ構築では機器購入や工事手配の前に綿密な設計や検証が不可欠ですが、AWS と CloudFormation を使えば、まず作ってみて評価しながら何度でもスクラップアンドビルドを繰り返すことができます。ウォーターフォールからアジャイルへの進化ですね。

## テンプレートとスタック
1つのテンプレートで定義されたリソースの集合は「スタック」という単位で管理され、削除の操作をするとスタック内のリソースがまとめて削除されます。だから手作業での削除もれによる予定外の請求なんかもなくなります。
またテンプレートを修正してスタックを更新すれば、自動的に差分だけを追加／更新／削除してくれます。

- [ユーザーガイド: AWS CloudFormation の概念](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-whatis-concepts.html)

## テンプレートの例
CloudFormation のテンプレートは、以下のような YAML（またはJSON）で記述されたテキストファイルです。

```Yaml:
AWSTemplateFormatVersion: '2010-09-09'

# 作成するリソースを定義するセクション
Resources:
  MainVpc:
    Type: 'AWS::EC2::VPC'   # VPCを作成する
    Properties:
      CidrBlock: 172.16.0.0/16
```
これは CIDR が`172.16.0.0/16`の VPC を1つだけ定義したスタックの例です。

- [ユーザーガイド: テンプレートの分析](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-anatomy.html)

# スタックの分割
しかし自動とはいえ、毎回全部を削除したくない場合もあります。その場合はスタックを適度に分割することができます。

## スタックを分割する理由
例えば Elastic IP（固定IPアドレス）は再作成するとアドレスが変わってしまいますし、サーバにアップロードしたファイルやログが毎回消えてしまいます。

またスタックを分割しないと、1つのテンプレートが際限なく巨大化してしまうという問題もあります。

## ただ分割しただけでは困ること
以下はスタックに含まれるリソース間に依存関係がある例です。

```Yaml:VPCとサブネットのスタック
Resources:
  # 作成するVPCの定義
  MainVpc:   # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 172.16.0.0/16
  # VPCの中に作成するサブネットの定義
  MainSubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock: 172.16.1.0/24
      VpcId: !Ref MainVpc   # 上で作成したVPCのIDを設定する
```
サブネットを作成するためには親となる VCP を指定する必要があるので、この例では`MainVpc`という変数に格納された VPC の ID を、サブネットの`VpcId`プロパティに設定しています。

- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::VPC](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Subnet](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html)

このように、同じスタック中なら`Ref`という組み込み関数を使うことで変数に格納された値を参照することができます。

- [組み込み関数リファレンス: Ref](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html)

このスタックをそのまま VPC とサブネットに分割すると…

```Yaml:VPCのスタック
Resources:
  MainVpc:   # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 172.16.0.0/16
```

```Yaml:サブネットのスタック
Resources:
  MainSubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock: 172.16.1.0/24
      VpcId: !Ref MainVpc   # エラー！別のスタックの変数を参照できない
```
サブネットのスタック内ではVPC の`MainVpc`変数を参照することができないために、サブネットの親となる VPC を指定することができません。

## スタック間で値を受け渡す方法

固定値であれば両方の`Parameters:`セクションで同じ値を定義してもよいのですが、`MainVpc`の値は VPC を作成する時に動的に割り当てられる ID なのでこの方法は使えません。

この場合は VPC のスタックを作成する時に ID の値をエクスポートしておけば、サブネットのスタックを作成する時に値をインポートすることができるようになります。

と言っても値を記憶するために記憶領域の用意や保存などの処理を自分でする必要はなく、エクスポートした変数の名前と値を CloudFormation が自動的に記憶してくれるようになっています。

```Yaml:VPCのスタック（VPCのIDをエクスポートする）
Resources:
  MainVpc:   # VPC作成処理の戻り値として、変数'MainVpc'にVPCのIDが格納される
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 172.16.0.0/16

# エクスポートするためのセクション
Outputs:
  MainVpc:
    Value: !Ref MainVpc  # 作成されたVPCのIDを
    Export:
      Name: main-vpc-id  # この名前でエクスポートする
```

```Yaml:サブネットのスタック（VPCのIDをインポートする）
Resources:
  MainSubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock: 172.16.1.0/24
      VpcId: {'Fn::ImportValue': 'main-vpc-id'} # 同じ名前でVPCのIDをインポートする
```
エクスポートした値をインポートするために組み込み関数の`ImportValue`を使用します。

- [チュートリアル: 別の AWS CloudFormation スタックのリソース出力を参照する](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html)
- [組み込み関数リファレンス: Fn::ImportValue](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html)

## もうちょっと現実的な例
少しだけ複雑な例として、以下の表のようにスタックを分割する場合を考えます。「インポートする値」に記載のあるスタックは、その値をエクスポートするスタックに依存することになります。

| スタック | エクスポートする値 | インポートする値 |
| ----- | ---- | ---- |
| Elastic IP | (A) EIPの`AllocationId` | |
| VPC とインターネットゲートウェイ | (B) VPCのID<br/>(C) インターネットゲートウェイのID | |
| 公開用サブネットとルートテーブル | (D) サブネットのID | (B) VPCのID<br/>(C) インターネットゲートウェイのID |
| 公開用 EC2 インスタンスとセキュリティグループ | | (A) EIPの`AllocationId`<br/>(B) VPCのID<br/>(D) サブネットのID |

なお、EC2 インスタンスへのアクセスに必要なキーペアはあらかじめコンソールで作成しておきます。

- [ユーザーガイド: EC2 キーペアを作成する](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-console-create-keypair.html)

### CloudFormation コンソールでスタックを作成する
以下はこれらの値を受け渡すスタックの実装例です。

各スタックのテンプレートを YAML ファイルに保存し、AWS CloudFormation のコンソールでファイルをアップロードすることでスタックを作成することができます。

- [ユーザーガイド: AWS CloudFormation コンソールの使用](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-using-console.html)

### Elastic IP
作成した Elastic IP から`AllocationId`というプロパティを取得するために`GetAtt`という組み込み関数を使用します。

```Yaml:eip-stack.yml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  # Elastic IPを作成する
  MainEip:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc

Outputs:
  MainEipAllocationId:
    # 組み込み関数GetAttを使用してAllocationIdプロパティを取得しエクスポートする
    Value: !GetAtt MainEip.AllocationId
    Export:
      Name: 'main-eip-allocationid'
```
スタックの作成に成功すると、コンソールの「出力」タブでエクスポートされた値を確認することができます。
![eip-output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/fca6c7f8-ee03-9992-495e-2565260614f4.png)

- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::EIP](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip.html)
- [組み込み関数リファレンス: Fn::GetAtt](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html)

### VPC とインターネットゲートウェイ
VPC とインターネットゲートウェイを作成して両者を関連付けてから、両方のIDをエクスポートします。

```Yaml:main-vpc-stack.yml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  # VPCを作成する
  MainVpc:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 172.16.0.0/16
      Tags:
        - Key: 'Name'
          Value: 'main-vpc'
  # インターネットゲートウェイを作成する
  MainInetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
      - Key: 'Name'
        Value: 'main-igw'
  # 作成したVPCとインターネットゲートウェイを関連付ける
  MainVpcGatewayAttachment:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      InternetGatewayId: !Ref MainInetGateway
      VpcId: !Ref MainVpc

Outputs:
  # 作成したVPCのIDをエクスポートする
  MainVpc:
    Value: !Ref MainVpc
    Export:
      Name: 'main-vpc-id'
  # 作成したインターネットゲートウェイのIDをエクスポートする
  MainInetGateway:
    Value: !Ref MainInetGateway
    Export:
      Name: 'main-igw-id'
```
作成した VPC とインターネットゲートウェイの ID がエクスポートされました。
![main-vpc-output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/74547729-64bf-ba21-3f39-eb4c5a8bda2a.png)

- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::VPC](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::InternetGateway](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internetgateway.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::VPCGatewayAttachment](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc-gateway-attachment.html)

### 公開用サブネットとルートテーブル
サブネット自体は、CIDR とインポートした VPC ID を設定するだけで作成することができます。EC2 インスタンスのスタックで必要になるので、サブネットの ID をエクスポートします。

ルートテーブルは少し複雑で、以下のような構成になります。

- ルートテーブル本体を、インポートした VPC ID を設定して作成する
- ルートテーブルのデフォルトルート（インターネットへの出口）を作成し、インポートしたインターネットゲートウェイ ID をターゲットに設定する
- サブネットとルートテーブルを関連付ける

```Yaml:public-subnet-stack.yml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  # サブネットを作成する
  PublicSubnet:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock: 172.16.1.0/24
      # このサブネット内のEC2起動時にパブリックIPアドレスを割り当てる
      MapPublicIpOnLaunch: true
      VpcId: {'Fn::ImportValue': 'main-vpc-id'}  # VPCのIDをインポート
      Tags:
        - Key: 'Name'
          Value: 'public-subnet'
  # ルートテーブルを作成する
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: {'Fn::ImportValue': 'main-vpc-id'}  # VPCのIDをインポート
      Tags:
        - Key: 'Name'
          Value: 'public-rtb'
  # ルートテーブルのデフォルトルート（インターネットへの出口）を作成する
  PublicDefaultRoute:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      # インターネットゲートウェイのIDをインポート
      GatewayId: {'Fn::ImportValue': 'main-igw-id'}
  # ルートテーブルとサブネットを関連付ける
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

Outputs:
  # 作成したサブネットのIDをエクスポートする
  PublicSubnet:
    Value: !Ref PublicSubnet
    Export:
      Name: !Sub 'public-subnet-id'
```
作成したサブネットとルートテーブルが関連付けられ、デフォルトの送信先`0.0.0.0/0`のターゲットとしてインポートしたインターネットゲートウェイのIDが設定されました。
![public-subnet-rtb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/14033668-feac-4a6c-0a66-b1ffe5efe425.png)

作成したサブネットのIDがエクスポートされました。
![public-subnet-output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/440820be-e3ed-caff-a9ce-e453497dce40.png)

- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Subnet](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::RouteTable](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route-table.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Route](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::SubnetRouteTableAssociation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet-route-table-assoc.html)

### 公開用 EC2 インスタンスとセキュリティグループ
公開用サーバーとして固定IPアドレスを持ち、インターネットからアクセス可能な EC2 インスタンスを作成するために、最初に作成した EIP（固定IPアドレス）を関連付けます。

また HTTP/HTTPS 用の受信ルールを持つセキュリティグループ（ファイヤーウォール）を作成して関連付けます。

```Yaml:public-ec2-stack.yml
AWSTemplateFormatVersion: '2010-09-09'

# パラメータを定義するセクション
Parameters:
  # EC2のキーペアの名前を定義し、スタック作成時にコンソールで指定する
  Ec2KeyName:
    Type: 'AWS::EC2::KeyPair::KeyName'

Resources:
  # 公開用EC2インスタンスを作成する
  PublicEc2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-00d101850e971728d
      InstanceType: 't2.micro'
      KeyName: !Ref Ec2KeyName
      # ストレージを作成する
      BlockDeviceMappings:
        - DeviceName: '/dev/xvda'
          Ebs:
            VolumeType: 'gp2'
            VolumeSize: 8
      # ネットワークインタフェースとして、下で作成するENIを設定する
      NetworkInterfaces:
        - NetworkInterfaceId: !Ref PublicEni
          DeviceIndex: '0'
      Tags:
        - Key: 'Name'
          Value: 'public-ec2-instance'
  # 公開用Elastic Network Interface (ENI) を作成する
  PublicEni:
    Type: 'AWS::EC2::NetworkInterface'
    Properties:
      # 下で作成するセキュリティグループ（ファイヤーウォール）を設定する
      GroupSet:
        - !Ref PublicSecurityGroup
      PrivateIpAddress: 172.16.1.4
      # インポートした公開用サブネットのIDを設定する
      SubnetId: {'Fn::ImportValue': 'public-subnet-id'}
      Tags:
        - Key: 'Name'
          Value: 'public-eni'
        - Key: 'Interface'
          Value: 'eth0'
  # 作成したENIにインポートしたEIP（固定IPアドレス）を関連付ける
  PublicEIPAssociation1:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: {'Fn::ImportValue': 'main-eip-allocationid'}
      NetworkInterfaceId: !Ref PublicEni

  # セキュリティグループを作成する
  PublicSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: public-sg
      GroupDescription: "SecurityGroup for Public EC2 instances"
      # インバウンドのルールを作成する
      SecurityGroupIngress:
      # インターネットからのHTTP通信を受信する
      - IpProtocol: tcp
        FromPort: '80'
        ToPort: '80'
        CidrIp: 0.0.0.0/0
      # インターネットからのHTTPS通信を受信する
      - IpProtocol: tcp
        FromPort: '443'
        ToPort: '443'
        CidrIp: 0.0.0.0/0
      Tags:
        - Key: 'Name'
          Value: 'public-sg'
      VpcId: {'Fn::ImportValue': 'main-vpc-id'}  # VPCのIDをインポートして設定する
```
CloudFormation のコンソールでスタックを作成する時に、作成済みのキーペアをパラメータとして指定します。

- [ユーザーガイド: パラメータ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html)

![public-ec2-params.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/c9d575c9-b96f-3a03-6e00-11d033ea8697.png)

スタックの作成が完了すると、EC2 インスタンスと Elastic IP の関連付け、およびインターネットからの HTTP/HTTPS トラフィックを許可するセキュリティグループが作成されました。
![public-ec2-sg-rules.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/921dfce1-231d-98c1-ddbe-1aeae90c73b4.png)

- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::Instance](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::NetworkInterface](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-interface.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::EIPAssociation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip-association.html)
- [AWS リソースおよびプロパティタイプのリファレンス: AWS::EC2::SecurityGroup](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html)

# あとがき
AWS 自体の複雑な仕組みと CloudFormation の理解を同時に進めるのは大変ではありますが、良質な情報がとにかく豊富であることは大変ありがたいです。
参考: [ユーザーガイド: AWS CloudFormation とは](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)

時にはリソースどうしの複雑な関係を整理して理解するために、クラス図を作ったりしました。
[AWSを始めたらVPCとEC2の複雑さに悩んだのでクラス図にしてみた](https://qiita.com/3244/items/c78ba88d10326a162c88)

さらに少しずつでも実際に構築して動きを体感することで理解を進めるために、CloudFormation を使ったスクラップアンドビルドが大変有効だと感じたので、今回の記事を作成しました。