CloudFormationをゼロから勉強する。（その５：マッピングと条件式）

# はじめに
今回は`Mappings`セクションと`Conditions`セクションを使って、環境ごとの値設定と、条件関数による切り替えを実施してみようと思います。

- 【前】[CloudFormationをゼロから勉強する。（その４：コマンドからの操作）](https://qiita.com/sakai00kou/items/cfc3c43857ca4d9def18)
- 【次】[CloudFormationをゼロから勉強する。（その６：Transformによるインクルード）](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)

# Mappingsセクションについて
キーと名前付きの値が対応付けられたグループ化機能となります。

「[AWS CloudFormationユーザガイド](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html)」ではリージョンごとに`AMI ID`をマッピングした例が紹介されております。

```yaml:Mappingsサンプル（抜粋）
Mappings: 
  RegionMap: 
    us-east-1: 
      "HVM64": "ami-0ff8a91507f77f867"
    us-west-1: 
      "HVM64": "ami-0bdb828fd58c52235"
```

また、`Mappings`セクションで対応付けた値は以下の様に`Fn::FindInMap:`（短縮形`!FindInMap`）関数で呼び出すことができます。

```yaml:値の呼び出し（抜粋）
AWSTemplateFormatVersion: "2010-09-09"
Mappings: 
  RegionMap: 
    us-east-1:
      HVM64: ami-0ff8a91507f77f867
    us-west-1:
      HVM64: ami-0bdb828fd58c52235
Resources: 
  myEC2Instance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
      InstanceType: m1.small
```

# Conditionsセクションについて
条件式を記載するセクションとなります。

他の開発言語と比べると使用できる条件は少ないですが、よほど凝ったことをしなければ問題にはならないでしょう。

|内容|条件関数|完全名関数の構文|短縮形の構文|
|:--|:--|:--|:--|
|AND条件|Fn::And|Fn::And: *[condition]*|!And *[condition]*|
|比較|Fn::Equals|Fn::Equals: *[value_1, value_2]*|!Equals *[value_1, value_2]*|
|IF条件|Fn::If|Fn::If: *[condition_name, value_if_true, value_if_false]*|!If *[condition_name, value_if_true, value_if_false]*|
|否定|Fn::Not|Fn::Not: *[condition]*|!Not *[condition]*|
|OR条件|Fn::Or|Fn::Or: *[condition, ...]*|!Or *[condition, ...]*|

使い方としては`Conditions`セクションで判定方法を記載して、他のセクションでリソース作成の条件等で、`Conditions`セクションで指定した名前を呼び出して使用します。

最初いまいちイメージが掴みづらかったのですが、シェルスクリプトの`if文`でいう`testコマンド`による判定部分が`Conditions`セクションに当たり、後から`Condition名`として他セクションから呼び出します。

```yaml:Conditionsサンプル（抜粋）
Conditions: 
  CreateProdResources: !Equals [ !Ref EnvType, prod ]
Resources: 
  EC2Instance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
  MountPoint: 
    Type: "AWS::EC2::VolumeAttachment"
    Condition: CreateProdResources
    Properties: 
      InstanceId: 
        !Ref EC2Instance
      VolumeId: 
        !Ref NewVolume
      Device: /dev/sdh
  NewVolume: 
    Type: "AWS::EC2::Volume"
    Condition: CreateProdResources
    Properties: 
      Size: 100
      AvailabilityZone: 
        !GetAtt EC2Instance.AvailabilityZone
Outputs: 
  VolumeId: 
    Condition: CreateProdResources
    Value: 
      !Ref NewVolume
```

# 今回作成する構成
今回は以下のようなシナリオを想定して構成を作ってみようと思います。

1. 開発環境か本番環境かをユーザに選択させる。
2. 選択した環境用のキーペアを`EC2インスタンス`に紐づける。
3. 本番環境の場合、追加で`EC2インスタンス`を作成して2台構成とする。

## 事前準備
今回は予め各環境用のキーペアを作成しておきます。

|環境|キーペア名|
|:--|:--|
|開発環境用キーペア|staging_key|
|本番環境用キーペア|production_key|

## Parametersセクションへの設定追加
今回も前回作成したテンプレートとパラメータファイルをベースに作成していきます。

<details><summary>前回のテンプレート（展開して下さい）</summary><div>

```yaml:/home/ec2-user/cloudformation.yaml
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  'AWS::CloudFormation::Designer':
    e1390660-013a-4523-9893-ccd6074e430f:
      size:
        width: 190
        height: 190
      position:
        x: 390
        'y': 90
      z: 0
      embeds:
        - e6cc612e-5a0f-459b-8329-3a5852613031
    e6cc612e-5a0f-459b-8329-3a5852613031:
      size:
        width: 140
        height: 140
      position:
        x: 410
        'y': 130
      z: 1
      parent: e1390660-013a-4523-9893-ccd6074e430f
      iscontainedinside:
        - e1390660-013a-4523-9893-ccd6074e430f
Resources:
  EC2VPC1WR8B:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VPCRange
    Metadata:
      'AWS::CloudFormation::Designer':
        id: e1390660-013a-4523-9893-ccd6074e430f
  EC2SJOWQ:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref EC2VPC1WR8B
      CidrBlock: !Ref SubnetRange
    Metadata:
      'AWS::CloudFormation::Designer':
        id: e6cc612e-5a0f-459b-8329-3a5852613031
  EC2I3Q8S8:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      NetworkInterfaces:
        - SubnetId: !Ref EC2SJOWQ
          DeviceIndex: 0
Parameters:
  VPCRange:
    Type: String
    Description: "VPC Subnet Range"
  SubnetRange:
    Type: String
    Description: "Subnet Range"
Outputs:
  VPCRegion:
    Description: "Region Name"
    Value: !Ref AWS::Region
```
</div></details>

<details><summary>前回のパラメータファイル（展開して下さい）</summary><div>

```json:/home/ec2-user/param.json
[
  { "ParameterKey" : "VPCRange", "ParameterValue" : "172.24.0.0/16" },
  { "ParameterKey" : "SubnetRange", "ParameterValue" : "172.24.0.0/24" }
]
```
</div></details>

今回は環境をユーザに選択させたいため`AllowedValues`で選択させるようにしようと思います。

ちなみに`AllowedValues`を設定するとデザイナー画面から入力する際に以下のように選択式にすることができます。

![capture_11092020_091321.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/916003a5-8eb7-efae-8c20-44b552c11a1b.jpeg)

まぁ、CLIからやるのであればあまり関係ありませんが・・・

前回`VPCRange`と`SubnetRange`のパラメータを作成しているので、その下に`EnvType`を追記してください。

```yaml:本番環境、開発環境の選択
Parameters:
  EnvType:
    Type: String
    Description: "Production or Staging"
    AllowedValues: ["Production", "Staging"]
```

## Mappingsセクションの記載
事前作業として作成した各環境のキーペアを指定するように`Mappings`セクションを作成します。

```yaml:各環境ごとのキーペア指定
Mappings:
  Keypair:    ★Map名
    productionkey:　　★キー値
      "KEYPAIR": "production_key"    ★値
    stagingkey:
      "KEYPAIR": "staging_key"
```

`Mappings`セクションで指定した値は`!FindInMap ["Map名", "キー値", "値"]`で取得します。

## Conditionsセクションの記載
先ほど`Parameters`セクションで作成した`EnvType`を呼び出し、本番環境を選択しているかどうかを判定します。

```yaml:本番環境判定
Conditions:
  EnvSelect: {"Fn::Equals": [{"Ref" : "EnvType"}, "Production"]}
```

## Resourcesセクションの記載
`Conditions`セクションで作成した本番環境の判定条件を、本番環境用キーペアの作成と、2台目の`EC2インスタンス`作成条件として使用します。

### 本番環境用キーペアの作成条件
キーペアを指定する`EC2インスタンス`リソースの`KeyName`に`Conditions`セクションで作成した条件を使用して本番環境用キーペアと開発環境用キーペアを指定するようにしたいと思います。

前回のテンプレートを使用して`KeyName`を以下のように指定します。

```yaml:キーペアの判定
  EC2I3Q8S8:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      KeyName: {Fn::If: [EnvSelect, !FindInMap [ Keypair, productionkey, KEYPAIR ], !FindInMap [ Keypair, stagingkey, KEYPAIR ]]}
      NetworkInterfaces:
        - SubnetId: !Ref EC2SJOWQ
          DeviceIndex: 0
```

上記は`Fn::If`で条件関数を使用し、判定条件を`EnvSelect`で呼び出し、`EnvSelect`の結果が`真`の場合は`Mappings`セクションの`productionkey`のキーペア名を指定、`偽`の場合は`Mappings`セクションの`stagingkey`のキーペア名を指定する条件式となります。

### 本番環境用インスタンスの作成
本番環境の場合に、2台目の`EC2インスタンス`を作成するようにします。

下記1行目のインスタンス名以外、「本番環境用キーペアの作成条件」で作成した内容とほぼ同じですが、最終行の`Condition`で`EnvSelect`が`真`の場合にのみ`EC2インスタンス`を作成するようにします。

```yaml:本番環境用EC2インスタンスの作成
  EC2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      KeyName: {Fn::If: [EnvSelect, !FindInMap [ Keypair, productionkey, KEYPAIR ], !FindInMap [ Keypair, stagingkey, KEYPAIR ]]}
      NetworkInterfaces:
        - SubnetId: !Ref EC2SJOWQ
          DeviceIndex: 0
    Condition: EnvSelect
```

## テンプレートの実行
今回もCLIで実行するため、パラメータファイルにも今回追加した設定を記載して実行していきます。

<details><summary>完成したテンプレート（展開して下さい）</summary><div>

```yaml:/home/ec2-user/cloudformation.yaml
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  'AWS::CloudFormation::Designer':
    e1390660-013a-4523-9893-ccd6074e430f:
      size:
        width: 190
        height: 190
      position:
        x: 390
        'y': 90
      z: 0
      embeds:
        - e6cc612e-5a0f-459b-8329-3a5852613031
    e6cc612e-5a0f-459b-8329-3a5852613031:
      size:
        width: 140
        height: 140
      position:
        x: 410
        'y': 130
      z: 1
      parent: e1390660-013a-4523-9893-ccd6074e430f
      iscontainedinside:
        - e1390660-013a-4523-9893-ccd6074e430f
Resources:
  EC2VPC1WR8B:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VPCRange
    Metadata:
      'AWS::CloudFormation::Designer':
        id: e1390660-013a-4523-9893-ccd6074e430f
  EC2SJOWQ:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref EC2VPC1WR8B
      CidrBlock: !Ref SubnetRange
    Metadata:
      'AWS::CloudFormation::Designer':
        id: e6cc612e-5a0f-459b-8329-3a5852613031
  EC2I3Q8S8:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      KeyName: {Fn::If: [EnvSelect, !FindInMap [ Keypair, productionkey, KEYPAIR ], !FindInMap [ Keypair, stagingkey, KEYPAIR ]]}
      NetworkInterfaces:
        - SubnetId: !Ref EC2SJOWQ
          DeviceIndex: 0
  EC2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      KeyName: {Fn::If: [EnvSelect, !FindInMap [ Keypair, productionkey, KEYPAIR ], !FindInMap [ Keypair, stagingkey, KEYPAIR ]]}
      NetworkInterfaces:
        - SubnetId: !Ref EC2SJOWQ
          DeviceIndex: 0
    Condition: EnvSelect
Parameters:
  VPCRange:
    Type: String
    Description: "VPC Subnet Range"
  SubnetRange:
    Type: String
    Description: "Subnet Range"
  EnvType:
    Type: String
    Description: "Production or Staging"
    AllowedValues: ["Production", "Staging"]
Outputs:
  VPCRegion:
    Description: "Region Name"
    Value: !Ref AWS::Region
Mappings:
  Keypair:
    productionkey:
      "KEYPAIR": "production_key"
    stagingkey:
      "KEYPAIR": "staging_key"
Conditions:
  EnvSelect: {"Fn::Equals": [{"Ref" : "EnvType"}, "Production"]}
```
</div></details>

<details><summary>完成したパラメータファイル（開発環境向け）（展開して下さい）</summary><div>

```json:/home/ec2-user/param.json
[
  { "ParameterKey" : "VPCRange", "ParameterValue" : "172.24.0.0/16" },
  { "ParameterKey" : "SubnetRange", "ParameterValue" : "172.24.0.0/24" },
  { "ParameterKey" : "EnvType", "ParameterValue" : "Staging" }
]
```
</div></details>

### 開発環境用EC2インスタンスの作成
上記のパラメータファイルは`EnvType`の`ParameterValue`に`Staging`を指定しているので、以下コマンドを実行すると、開発環境用のキーペアが指定された`EC2インスタンス`が作成されます。

```shell:テンプレートの実行
aws cloudformation create-stack --template-body file:///home/ec2-user/cloudformation.yaml --stack-name stack-test --parameters file:///home/ec2-user/param.json
```

`CloudFormation`のリソース画面と`EC2`のインスタンス画面を見ると、開発環境用のキーペアが付与された`EC2インスタンス`が1台作成されているのが確認できます。
![capture_13092020_125253.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/a43c9afa-043b-0adc-a2d6-36a95f69e529.jpeg)

![capture_13092020_125709.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/30edbe0a-a904-0c59-a058-ee76a6018be8.jpeg)

次の作業のためにいったんテンプレートを削除しておきます。

```shell:テンプレートの削除
aws cloudformation delete-stack --stack-name stack-test
```

### 本番環境用EC2インスタンスの作成
パラメータファイルの`EnvType`の`ParameterValue`を`Production`に書き換えて再度テンプレートを作成します。

<details><summary>完成したパラメータファイル（本番環境向け）（展開して下さい）</summary><div>

```json:/home/ec2-user/param.json
[
  { "ParameterKey" : "VPCRange", "ParameterValue" : "172.24.0.0/16" },
  { "ParameterKey" : "SubnetRange", "ParameterValue" : "172.24.0.0/24" },
  { "ParameterKey" : "EnvType", "ParameterValue" : "Production" }
]
```
</div></details>

```shell:テンプレートの実行
aws cloudformation create-stack --template-body file:///home/ec2-user/cloudformation.yaml --stack-name stack-test --parameters file:///home/ec2-user/param.json
```

`CloudFormation`のリソース画面と`EC2`のインスタンス画面を見ると、本番環境用のキーペアが付与された`EC2インスタンス`が2台作成されているのが確認できます。

![capture_13092020_132356.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/8e0fd722-3530-ae8e-d488-6ea5297e4f65.jpeg)

![capture_13092020_132556.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/fce93493-2213-1aac-7a13-2a5050da6819.jpeg)

# おわりに
`Mappings`と`Conditions`を使用することで、1つのソースを複数の環境で使いまわすことができるので、特定の環境だけ追加した設定を反映し忘れたといったことが防げそうです。

次回はまだ使っていない`Transform`セクションを使ってみようと思います。