CloudFormation備忘録

この記事では、CloudFormationのエラー対処の過程で学んだことを共有したいと思います。
# EC2インスタンスを作成したVPCサブネットに配置し、パブリックIPの自動割り当てを有効化する場合の記法
例えば、VPCとセキュリティグループを作成し、作成したVPCサブネット内にEC２インスタンスを割り当てたいとします。
また、パブリックIPを自動的に割り当てるためにNetworkInterfacesのプロパティであるAssociatePublicIpAddressを有効化します。

```yaml:invalid-stack.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  KeyName:
    Type: String
    Description: Key pair of the instance
  MyIpParameter:
    Type: String
    Description: Your current public IP address
    NoEcho: true
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: sample-vpc
# Subnet
  Subnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 172.16.0.0/24
      # ap-northeast-1aに配置
      AvailabilityZone: !Select
        - '0'
        - !GetAZs ''
      Tags:
        - Key: Name
          Value: sample-subnet-1a
  # SecurityGroup
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access via port 22, and allow all incoming HTTP (port 80) connections
      GroupName: WebServer-SG
      SecurityGroupIngress:
        - CidrIp: !Join ['', [!Ref MyIpParameter, '/32']]
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: WebServer-SG
  # Web server
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-08a8688fb7eacb171
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref Subnet1a
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      NetworkInterfaces:
        - DeviceIndex: 0
          AssociatePublicIpAddress: true
      Tags:
        - Key: Name
          Value: SampleWebServer
```
しかし、このテンプレートを使ってスタックを作成しようとすると、以下のようなエラーが発生し、ROLL_BACKとなってしまいます。
![create-stack-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/d7f0c2ea-2bfa-e100-5ab1-392d439e6f97.png)
このエラーメッセージは、ネットワークインターフェイスのサブネットとインスタンスレベルのサブネットを同時に指定しようとすると発生します。
原因となっているのは、以下の記述です。

```yml
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      # ...
      SubnetId: !Ref Subnet1a
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      NetworkInterfaces:
        - DeviceIndex: 0
          AssociatePublicIpAddress: true
```
cfn-lintでチェックすると、インスタンス直下のプロパティのSubnetIdはネットワークインターフェイスと同時に存在することはできないとのことです。
![err-msg-cfn-lint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/5af38d1e-2f68-2b0f-c6fc-3887e0c104b6.png)
以下のように、サブネットとセキュリティグループをネットワークインターフェイス内のプロパティとして指定するとエラーは解消します。

```yml:valid-stack.yml
# 他は全て同じ
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-08a8688fb7eacb171
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      NetworkInterfaces:
        - DeviceIndex: 0
          AssociatePublicIpAddress: true
          SubnetId: !Ref Subnet1a
          GroupSet:
            - !Ref EC2SecurityGroup
```
![success-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/e0754ae9-071c-dc68-51eb-d47bc80acccc.png)
意図した通り、パブリックIPが割り当てられると同時に、インスタンスがテンプレート内で作成したVPCサブネット内に配置されていることがわかります。
![success-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/663b4c96-b18f-dfd9-a76f-dcf2229423a1.png)
これは作成したVPCを指定すると発生するエラーです。
VPCを指定せずに、デフォルトVPCを使用する場合はエラーにはなりません。
# RDSのセキュリティグループ
RDSインスタンスのドキュメントを見てみると、次のような記載があります。
![rds-instance-dbsg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/87d2d69d-7626-889e-7652-35a1b72121af.png)
一見すると、RDSに適用するセキュリティグループは`DBSecurityGroups`というプロパティで指定するように見えます。
以下のテンプレートでRDSインスタンスを作成してみます。

```yml:invalid-rds.yml
AWSTemplateFormatVersion: 2010-09-09
Resources:
# Network
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: sample-vpc
# Private Subnet
  PrivateSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 172.16.0.0/24
      AvailabilityZone: !Select
        - '0'
        - !GetAZs ''
      Tags:
        - Key: Name
          Value: sample-private-1a
  PrivateSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 172.16.1.0/24
      AvailabilityZone: !Select
        - '1'
        - !GetAZs ''
      Tags:
        - Key: Name
          Value: sample-private-1c
# SecurityGroup
  RdsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: RDS-SG
      GroupDescription: allow connections via port 3306
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        IpProtocol: tcp
        FromPort: 3306
        ToPort: 3306
      VpcId: !Ref VPC
      Tags: 
        - Key: Name
          Value: RDS-SG
# Database
  RdsInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      DBInstanceIdentifier: my-sample-db
      DBName: sampledb
      DBSecurityGroups: 
        - !Ref RdsSecurityGroup
      DBSubnetGroupName: !Ref RdsSubnetGroup
      Engine: MySQL
      EngineVersion: 8.0.23
      MasterUsername: testuser
      MasterUserPassword: adminpass
      MultiAZ: false
      PubliclyAccessible: false
      StorageEncrypted: false
  RdsSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: DB Subnet Group
      DBSubnetGroupName: DBSubnetGroup
      SubnetIds:
        - !Ref PrivateSubnet1a
        - !Ref PrivateSubnet1c
```
しかし、このテンプレートを使用すると、以下のエラーが発生し、スタックの作成に失敗します。
DBSecurityGroupが存在しないと言われています。
![rds-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/a2ab796b-9560-92e5-b7c8-a257a7fe0c18.png)
CloudFormationの公式ドキュメントによると、`DBSecurityGroups`は古いリージョンとの下位互換のために残されている項目であり、現在は`VPCSecurityGroups`で指定せよとのことです。
![rds-document.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/45f09fae-87a1-34e8-167a-c88f16ff7919.png)
`DBSecurityGroups`を`VPCSecurityGroups`に置き換えたテンプレートを使うと、スタックの作成に成功します。

```yaml:valid-rds.yml
# 他は全て同じ
# Database
  RdsInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      DBInstanceIdentifier: my-sample-db
      DBName: sampledb
      VPCSecurityGroups:
        - !Ref RdsSecurityGroup
      DBSubnetGroupName: !Ref RdsSubnetGroup
      Engine: MySQL
      EngineVersion: 8.0.23
      MasterUsername: testuser
      MasterUserPassword: adminpass
      MultiAZ: false
      PubliclyAccessible: false
      StorageEncrypted: false
```
また、CloudFormation公式ドキュメントのRDSの欄を眺めてみると、`AWS::RDS::DBSecurityGroup`というリソースタイプが存在するようです。
![rds-document-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/16e13cc8-6150-f95d-8cc3-8e63e8e66659.png)
一見すると、`AWS::EC2::SecurityGroup`ではなく、これを使ってセキリュティグループを作成する必要があるかのように見えます。
しかし、ドキュメントの中身を見てみると、こちらもレガシー環境との下位互換のためだけに存在する項目で、現在は使用しないことが推奨されているようです。
![rds-document-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1000359/b51d6508-d6a8-90f4-2078-021f7874b8b9.png)
結論としては、RDSに適用するセキュリティグループは`AWS::EC2::SecurityGroup`として作成し、DBInstanceの`VPCSecurityGroups`プロパティで指定するということになります。
# 終わりに
このように、CloudFormationには、一見すると正しそうに見える記述なのになぜエラーになるのかわかりにくい要素があります。
ネット上にあるテンプレートを継ぎはぎして作成したり、ドキュメントの表面だけ眺めていると、思わぬエラーに直面しやすくなります。
エラーになった時は、以下のようなことが解決につながりやすいと感じました。
- 記述の間違い／タイポを疑う（特に配列）
- 参照の失敗を疑う
- レガシーな記法である可能性を疑う
- エラーメッセージと公式ドキュメントをよく読む
# 参考
以下の記事を参考にさせていただきました。
https://dev.classmethod.jp/articles/aws-cloudformation-networkinterfaces-and-instance-level-error/