EC2を柔軟に設定するためのCloudFormation Templateの作成

# 概要
本記事ではEC2をCloudFormationで作成する際に、様々なオプションに柔軟に対応する方法をご紹介します。

GUIでEC2を作成する場合、アタッチするEBSの数は簡単に増減出来ますよね？他にもVPCやSecurityGroupをリストから選択出来たり、EC2の作成ウィザードは非常に多くの項目を簡単に設定することが出来ます。  
一方で組織のポリシーとしてEC2にプリセットしたい設定がある場合（例：特定タグの付与やリソース名称の指定etc)などは、あらかじめ値を指定したCloudFormation Templateを用意・利用して作成することで実現することが出来ます。
マルチアカウント環境においてもAWS Service Catalogを使えば、メンバーアカウントに対してCloudFormation Templateを配布することが出来ますので、EC2の作成ウィザードではなくService Catalogのメニューから配布されたCloudFormation Templateを使ってEC2を作成することで組織のポリシーを満たしたリソースの作成が可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/761760d0-8a92-38d6-0838-eca3d4308b97.png)


https://docs.aws.amazon.com/ja_jp/servicecatalog/latest/adminguide/introduction.html


では、EC2の作成ウィザードのような設定の柔軟性を持つCloudFormation Templateを作るにはどうすればよいでしょうか。
以下ではその一例として、設定の種類毎のパラメータの書き方や条件に応じてEC2に複数のEBSをアタッチする方法をご説明します。

# ユースケース
- 設定の種類(Boolean型,複数項目から単一選択,複数項目から複数選択)に合わせたCloudFormation Templateでの記載方法を確認したい
- 複数の条件分岐を持たせたEC2作成用のCloudFormation Templateを用意したい

# 設定内容

## 全体
CloudFormation Templateの全文を記載します。
＊かなり行数があるためいくつか設定値をカットしています。
＊パラメータの整理のため、パラメータグループの利用をオススメします。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-interface-parametergroup.html

```
AWSTemplateFormatVersion: "2010-09-09"
Description: ""       
Parameters:
  Ec2ImageId:
    Type: AWS::EC2::Image::Id
  Ec2InstanceType:
    Description: Select EC2 InstanceType
    Type: String
    AllowedValues:  ["t3.micro", "t3.medium", "t3.large"]
  Ec2KeyName:
    Type: AWS::EC2::KeyPair::KeyName
  SubnetID:
    Type: AWS::EC2::Subnet::Id
  SecurityGroups:
    Type: "List<AWS::EC2::SecurityGroup::Id>"
  EbsVolume00Size:
    Type: Number
    Default: 10
  EbsVolume00Type:
    Type: String
    Default: gp2
    AllowedValues: [gp2, gp3, io1, io2, standard]
  EbsVolume01Size:
    Type: Number
    Default: 0
  EbsVolume01Type:
    Type: String
    Default: gp2
    AllowedValues: [gp2, gp3, io1, io2, standard]
  EbsVolume02Size:
    Type: Number
    Default: 0
  EbsVolume02Type:
    Type: String
    Default: gp2
    AllowedValues: [gp2, gp3, io1, io2, standard]
  InstanceProfile:
    Type: String
  DisableApiTermination: 
    Type: String
    Default: false
    AllowedValues: [true, false]
  Tenancy:
    Type: String
    Default: default
    AllowedValues: [default, dedicated, host]
  EBSEncryption:
    Type: String
    Default: true
    AllowedValues: [true, false]
  EBSOptimized:
    Type: String
    Default: false
    AllowedValues: [true, false]
  EBSDeleteOnTermination:
    Type: String
    Default: true
    AllowedValues: [true, false]

Conditions:
  AttachVolume01: !Not 
    - !Equals [!Ref EbsVolume01Size, "0"]
  AttachVolume02: !Not 
    - !Equals [!Ref EbsVolume02Size, "0"]

Resources:
    CreateEC2InstanceAmazonLinux2:
        Type: "AWS::EC2::Instance"
        Properties:
            ImageId: !Ref Ec2ImageId
            InstanceType: !Ref Ec2InstanceType
            KeyName: !Ref Ec2KeyName
            Tenancy: !Ref Tenancy
            EbsOptimized: !Ref EBSOptimized
            DisableApiTermination: !Ref DisableApiTermination         
            BlockDeviceMappings: 
              - DeviceName: "/dev/xvda"
                Ebs: 
                    Encrypted: !Ref EBSEncryption
                    VolumeSize: !Ref EbsVolume00Size
                    VolumeType: !Ref EbsVolume00Type
                    DeleteOnTermination: !Ref EBSDeleteOnTermination
              - DeviceName: "/dev/xvdb"
                NoDevice: !If [AttachVolume01,!Ref "AWS::NoValue", {}]
                Ebs:
                  !If
                    - AttachVolume01
                    - Encrypted: !Ref EBSEncryption
                      VolumeSize: !Ref EbsVolume01Size
                      VolumeType: !Ref EbsVolume01Type
                      DeleteOnTermination: !Ref EBSDeleteOnTermination
                    - !Ref "AWS::NoValue"
              - DeviceName: "/dev/xvdc"
                NoDevice: !If [AttachVolume02,!Ref "AWS::NoValue", {}]
                Ebs:
                  !If
                    - AttachVolume02
                    - Encrypted: !Ref EBSEncryption
                      VolumeSize: !Ref EbsVolume02Size
                      VolumeType: !Ref EbsVolume02Type
                      DeleteOnTermination: !Ref EBSDeleteOnTermination
                    - !Ref "AWS::NoValue"
            NetworkInterfaces:
              - AssociatePublicIpAddress: "false"
                DeviceIndex: "0"
                SubnetId: !Ref SubnetID
                GroupSet: 
                  - {"Fn::Join" : [ ",", { "Ref" : "SecurityGroups" }] }                
            IamInstanceProfile: !Ref InstanceProfile
```

## AWS固有のパラメータタイプ
一部のリソースの値はAWS固有のパラメータとして、アカウント内の既存の設定値を参照することが出ます。
このテンプレートの場合、KeyPair・Subnet・SecurityGroupはこのパラメータを利用しています。
```
  Ec2KeyName:
    Type: AWS::EC2::KeyPair::KeyName
  SubnetID:
    Type: AWS::EC2::Subnet::Id
  SecurityGroups:
    Type: "List<AWS::EC2::SecurityGroup::Id>"
```
CloudFormationのスタック作成画面から見ると以下のようになります。これはEC2の作成ウィザードにかなり近いですね。
![スクリーンショット 2024-05-08 075653.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/5a167e06-f41e-1b79-0bd5-20a57fae4fbd.png)

この値が参照されるのは以下の箇所のSubnetIdの部分、SubnetIdを調べるのは面倒なのでリストから選択出来るのはありがたいです。
```
            NetworkInterfaces:
              - AssociatePublicIpAddress: "false"
                DeviceIndex: "0"
                SubnetId: !Ref SubnetID
```
サポートされるAWSの固有パラメータタイプの一覧については以下のドキュメントをご確認ください。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html

## Boolean型の設定
EC2作成に関して言えば、このタイプのオプションが一番多いと思われます。
パラメータとしてはtrue/falseの二択として、どちらかを選択する形とします。
```
  DisableApiTermination: 
    Type: String
    Default: false
    AllowedValues: [true, false]
```
GUI上の表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/07e94850-a03a-8e09-4e28-8ddfc48a45ae.png)

リソース作成の箇所では!Refで参照します。
```
            DisableApiTermination: !Ref DisableApiTermination 
```


## 複数項目から単一選択を行う設定
上記の派生でtrue/falseではなく、許可された複数の項目から一つを選択する場合のオプションです。
例えば、上記で紹介したSubnetIdやInstanceTypeなどがこれに当たります。
SubnetIdは固有パラメータタイプを利用していましたが、AllowedValuesを利用することでリストから選択するように設定出来ます。
```
  Ec2InstanceType:
    Description: Select EC2 InstanceType
    Type: String
    AllowedValues:  ["t3.micro", "t3.medium", "t3.large"]
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/f3597ace-dc72-7525-94a1-eb2afd207453.png)

リソース作成の箇所では!Refで参照します。
```
            InstanceType: !Ref Ec2InstanceType
```

## 複数項目から複数選択を行う設定
今回ではSecurityGroupが該当します。SecurityGroupは固有パラメータタイプを利用していますが、固有パラメータタイプ以外を複数選択する場合はTypeに`List<Number>`または`CommaDelimitedList`を利用します。
```
  SecurityGroups:
    Type: "List<AWS::EC2::SecurityGroup::Id>"
```
CloudFormationのコンソールだと以下のように表示され、チェックボックスにチェックする形式になっているのが分かります。
![スクリーンショット 2024-05-08 075546.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/ac18c80c-2e7a-425d-2d57-bb0ff68f4e09.png)

リソース作成の箇所での指定は以下の通りです。複数の値を選択出来る場合はカンマ区切りが必要となりますので、**","区切りになるようJoin関数を活用しましょう。**
```
                GroupSet: 
                  - {"Fn::Join" : [ ",", { "Ref" : "SecurityGroups" }] } 
```
## 任意の文字列・数字を入力させる設定
ここではEBSのボリュームサイズやインスタンスプロファイルの指定に利用しています。Typeに`Number`もしくは`String`を利用します。
```
  EbsVolume00Size:
    Type: Number
    Default: 10
...
  InstanceProfile:
    Type: String
```

## 複数EBSのアタッチ
### Conditionの設定
Conditionは設定した条件に当てはまっていれば`true`、当てはまらなければ`false`を返す設定で、これを利用することで特定の条件の場合のみリソースを作成するよう設定出来ます。
ここでは、VolumeSizeとして`0`以外が入力されたときは`true`・`0`が入力されたときは`false`となるようなAttachVolume01(02)というConditionを用意します。
```
Conditions:
  AttachVolume01: !Not 
    - !Equals [!Ref EbsVolume01Size, "0"]
  AttachVolume02: !Not 
    - !Equals [!Ref EbsVolume02Size, "0"]
```
Conditionの詳細な説明については下記のドキュメントをご確認ください。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html

### EBSリソース作成の条件分岐
ここが非常にややこしいです。  
#### 全体(EBS)
```
              - DeviceName: "/dev/xvdb"
                NoDevice: !If [AttachVolume01,!Ref "AWS::NoValue", {}]
                Ebs:
                  !If
                    - AttachVolume01
                    - Encrypted: !Ref EBSEncryption
                      VolumeSize: !Ref EbsVolume01Size
                      VolumeType: !Ref EbsVolume01Type
                      DeleteOnTermination: !Ref EBSDeleteOnTermination
                    - !Ref "AWS::NoValue"
```
#### NoDevice設定
まずEC2インスタンス作成時に合わせてEBSを作成するには`AWS::EC2::Instance BlockDeviceMapping`を使用します。
このマッピング設定で`NoDevice: {}`を使用することでEBSが作成されない状態になります。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance-blockdevicemapping.html#cfn-ec2-instance-blockdevicemapping-nodevice

#### NoDeviceとIfの組み合わせ
次にCloudFormationのIf関数を利用します。If関数は`"Fn::If": [condition_name, value_if_true, value_if_false]`の形で利用されます。 


https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-if

今回は`NoDevice: !If [AttachVolume01,!Ref "AWS::NoValue", {}]`としていますので、
・AttachVolume01がtrueの場合
`NoDevice: !Ref "AWS::NoValue"`
・AttachVolume01がfalseの場合
`NoDevice: {}`
となります。

ここで登場する`!Ref "AWS::NoValue`は疑似パラメータ参照と呼ばれるものの一つで、!Ifの組み合わせることで対応するリソースプロパティを削除します。つまり今回の場合は`NoDevice`自体を削除するよう設定出来ます。

>AWS::NoValue
Fn::If 組み込み関数の戻り値として指定すると、対応するリソースプロパティを削除します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html

#### EBSリソースの作成
EBSの作成についてもIf関数を利用します。
・AttachVolume01がtrueの場合
指定した設定値でリソースを作成します。
```
                Ebs:
                    Encrypted: !Ref EBSEncryption
                    VolumeSize: !Ref EbsVolume01Size
                    VolumeType: !Ref EbsVolume01Type
                    DeleteOnTermination: !Ref EBSDeleteOnTermination
```

`NoDevice: !Ref "AWS::NoValue"`
・AttachVolume01がfalseの場合
`AWS::NoValue`を利用してリソースプロパティ自体を削除します。
```
EBS: !Ref "AWS::NoValue"
```

# まとめ
本記事ではEC2を作成するCloudFormation Templateを作成する場合を例として、設定の種類ごとのCloudFormationのパラメータの記載方法をご紹介しました。記事内で紹介している複数EBSのアタッチように関数等を組み合わせることで、CloudFormationでも柔軟な設定を実現することが出来ます。
一方、過度な条件分岐やパラメータ化を行うとテンプレート自体の可読性が下がってしまう可能性がありますので、複雑すぎる設定作業については無理にCloudFormationのみで実現しようとせず、適切なレベル感にとどめておくことをオススメします。