CloudFormationをゼロから勉強する。（その６：Transformによるインクルード）

# はじめに
今回はまだ試していなかった`Transform`のうち、他テンプレートのインクルード機能を使ってみようと思います。

- 【前】[CloudFormationをゼロから勉強する。（その５：マッピングと条件式）](https://qiita.com/sakai00kou/items/f562f02fe0fc8719ef65)
- 【次】[CloudFormationをゼロから勉強する。（その７：変更セットとドリフト検出）](https://qiita.com/sakai00kou/items/5a7ebb0898ea3eab7239)

# Transformセクションについて
`CloudFormation`から`Lambda`関数を呼び出す場合や、他のテンプレートをインクルードする場合に使用します。

今回使う他テンプレートのインクルードは、テンプレート内のどこでも呼び出すことができ、呼び出す位置によって若干構文が変わってきます。

ただ、インクルードで使う場合はトップレベルで使う場合でも`Fn::Transform`だけ覚えておけばよいと思います。（理由は後述）

```yaml:Transformサンプル（トップレベルで使用する場合の構文）
Transform:
  Name: 'AWS::Include'
  Parameters:
    Location: 's3://MyAmazonS3BucketName/MyFileName.yaml'
```

```yaml:Transformサンプル（他セクションで使用する場合の構文）
Fn::Transform:
  Name: 'AWS::Include'
  Parameters:
    Location: 's3://MyAmazonS3BucketName/MyFileName.yaml'
```

# Transform設定があるテンプレートの実行方法
`Transform`を含むテンプレートは今までのように`aws cloudformation create-stack`コマンドでは実行できず、`aws cloudformation package`コマンドでS3のバケットにインクルードするファイルをアップロードする必要があります。

`aws cloudformation package`コマンドでアップロード後は、`aws cloudformation deploy`コマンドでスタックを作成・実行します。

`create-stack`も`deploy`もどちらもスタックを作成できますが、変換を伴うテンプレートの実行（今回の場合、`Transform`による`Location`の変換）を行う場合は`deploy`で行う必要があるようです。

# Transformでインクルードする際の注意点
メインのテンプレートから呼び出すファイルですが、基本的にはコードの一部分をカット＆ペーストして別ファイルとしてインクルードすることができますがいくつか注意しなければいけない点があるので以下記載します。

## １．インクルードファイルの書式
インクルードファイルでは`!Ref`などの組み込み関数や`!FindInMap`のような短縮形の記載は使えないようで、`Ref:`や`Fn::FindInMap:`のような記載方法に書き直す必要があります。

また、`{}`や`[]`を使った記載もフォーマットエラーとなります。

`YAML`の書式についてあまり詳しくありませんが、いわゆる`ブロックスタイル`の書式でないとインクルードファイルは読み込んでくれないようです。

そのため、上記の書式が使われている部分を抜き出してインクルードファイルとする場合は、別の記載方法に変換する必要があります。

## ２．同じインデントレベルでの複数インクルードファイルの指定
同じ構造内で同じインデントレベルでの複数`Transform`定義の指定はできません。

例えば、`Resources`セクションで`EC2インスタンス`の定義と`サブネット`の定義を`Transform`でインクルードしようとしても最後に記載したインクルードファイルの定義しか反映されません。

また、`Location`に2つ書いた場合は、実行時に失敗します。

そのため、同じインデントレベルで複数インクルードする場合は、1つのファイルにまとめるか、構成を変更する必要があります。

```yaml:最後のインクルードのみ実行
Resources:
  Fn::Transform:  ★実行されない
    Name: AWS::Include
    Parameters:
      Location:
        - s3://[S3バケット名]/c102fec7b84d7dc46b57e4ea6c3ecc85
  Fn::Transform:  ★実行される
    Name: AWS::Include
    Parameters:
      Location:
        - s3://[S3バケット名]/1a887907495d757aa66496d127a9d9ae
```

```yaml:実行不可
Resources:
  Fn::Transform:  ★以下の書き方はエラーで失敗
    Name: AWS::Include
    Parameters:
      Location:
        - s3://[S3バケット名]/c102fec7b84d7dc46b57e4ea6c3ecc85
        - s3://[S3バケット名]/1a887907495d757aa66496d127a9d9ae
```

## ３．トップレベルでTransform指定した場合の変換
`Transform`でインクルードファイルを指定した場合、前述の`package`コマンドを実行してインクルードファイルをアップロードすると、`Location`のファイル名がアップロード先の`S3URL`に変換されます。

但し、トップレベルでの記載方法で`Transform`を設定すると、`package`コマンドで`Location`部分が変換されないようです。

`Fn::Transform`の記載でもトップレベルで設定できるので、インクルードで`Transform`を使う場合は`Fn::Transform`だけ使えばよいかと思います。

```yaml:Transformの記載でpackageコマンド実行した場合の例
AWSTemplateFormatVersion: 2010-09-09
Transform:
  Name: AWS::Include
  Parameters:
    Location: description.yaml  ★変換されない
```

```yaml:Fn：：Transformの記載でpackageコマンド実行した場合の例
AWSTemplateFormatVersion: 2010-09-09
Fn::Transform:
  Name: AWS::Include
  Parameters:
    Location: s3://[S3バケット名]/b7a2955ca33cb3908c36b49284fd487a  ★変換された
```

# 今回作成する構成
今回も今までと同様、[前回](https://qiita.com/sakai00kou/items/f562f02fe0fc8719ef65)のテンプレートを基に作成していきます。

前回からの追加要素として以下を追加します。

1. デザイナー画面からのパラメータ入力項目を分かりやすく整える。
2. EC2インスタンスの記述を`Transform`で別ファイルからインクルードする。

## パラメータ入力画面の整理
大体コマンドで実行しているのであまり関係ないのですが、`Metadata`セクションにパラメータ入力の順番等を記載することでパラメータ入力画面の項目を整えることができるようなので実装してみようと思います。

`MetaData`セクションに`AWS::CloudFormation::Interface`を設定することで画面整理できます。

```yaml:MetadataKeyによるパラメータ入力項目整理
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCRange
          - SubnetRange
      - Label:
          default: "EC2 Configuration"
        Parameters:
          - EnvType
    ParameterLabels:
      VPCRange:
        default: "Input VPC Range"
      SubnetRange:
        default: "Input Subnet Range"
      EnvType:
        default: "Select Environment"
```

- MetaData記載なし
![default.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/b3ecc4a4-02af-7123-85c6-66b10776fe1b.jpeg)
- MetaData記載あり
![metadatakey.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/b2832e28-4cbe-8867-9a89-8134fa3a6afe.jpeg)

尚、上記の`ParameterLabels`は入力しないと`Metadata記載なし`の場合と同様、`Parameters`セクションの項目名が表示されます。

今まで入力項目の順番がバラバラになるのが気になっていましたが、これで運用者がGUI画面上から入力する場合でも入力間違いが減らせそうです。

## インクルードファイルの作成
前回のテンプレートから`EC2インスタンス`部分をカット＆ペーストします。

ただ、前述の通り、`{}`や`[]`などは使用できません。

そのため、短縮系や`{}`等使われている箇所を変換していきます。

### Ref短縮形の変換
前回のテンプレートでは、`!Ref`と`!FinfInMap`を短縮形で使用しているため、通常の構文で記載した場合は以下となります。（!FindInMapは後述）

```yaml:変換前の構文
SubnetId: !Ref EC2SJOWQ
```

```yaml:変換後の構文
SubnetId:
  - Ref: EC2SJOWQ
```

### 括弧とFindInMap短縮形の変換
前回のテンプレートでは`KeyName`の指定で括弧や短縮形を多用しています。

括弧や短縮形が多くて変換が難しいですが、括弧の部分をそれぞれ1つのグループとして、グループ内は同列の要素として変換した結果が以下となります。

```yaml:変換前の構文
KeyName: {Fn::If: [EnvSelect, !FindInMap [ Keypair, productionkey, KEYPAIR ], !FindInMap [ Keypair, stagingkey, KEYPAIR ]]}
```

```yaml:変換後の構文
KeyName:
  Fn::If:
    - EnvSelect
    - Fn::FindInMap:
      - Keypair
      - productionkey
      - KEYPAIR
    - Fn::FindInMap:
      - Keypair
      - stagingkey
      - KEYPAIR
```

### 完成したインクルードファイル
以下の内容を`ec2.yaml`としてローカルに保存しておきます。

<details><summary>ブロックスタイルに変換後のインクルードファイル（展開して下さい）</summary><div>

```yaml:ブロックスタイルに変換後のテンプレートファイル
EC2I3Q8S8:
  Type: 'AWS::EC2::Instance'
  Properties:
    ImageId: ami-0cc75a8978fbbc969
    InstanceType: t2.micro
    KeyName:
      Fn::If:
        - EnvSelect
        - Fn::FindInMap:
          - Keypair
          - productionkey
          - KEYPAIR
        - Fn::FindInMap:
          - Keypair
          - stagingkey
          - KEYPAIR
    NetworkInterfaces:
      - AssociatePublicIpAddress: "true"
        SubnetId:
          Ref: EC2SJOWQ
        DeviceIndex: 0
EC2Instance:
  Type: 'AWS::EC2::Instance'
  Properties:
    ImageId: ami-0cc75a8978fbbc969
    InstanceType: t2.micro
    KeyName:
      Fn::If:
        - EnvSelect
        - Fn::FindInMap:
          - Keypair
          - productionkey
          - KEYPAIR
        - Fn::FindInMap:
          - Keypair
          - stagingkey
          - KEYPAIR
    NetworkInterfaces:
      - SubnetId:
          Ref: EC2SJOWQ
        DeviceIndex: 0
  Condition: EnvSelect
```
</div></details>

## テンプレートファイルの修正
先ほどカット＆ペーストで切り出した`EC2インスタンス`のリソースが記載されていた箇所に`Transform`を記載します。

```yaml:Transformの記載（抜粋）
Resources:
  Fn::Transform:
    Name: AWS::Include
    Parameters:
      Location: "ec2.yaml"
```

## 完成したテンプレートの変換
完成したテンプレートファイルは以下となります。

この時点では`Include`の`Location`で指定しているファイル名はローカルで作成したファイル名（`ec2.yaml`）で問題ありません。

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

```yaml:/home/ec2-user/cloudformation.yaml
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCRange
          - SubnetRange
      - Label:
          default: "EC2 Configuration"
        Parameters:
          - EnvType
    ParameterLabels:
      VPCRange:
        default: "Input VPC Range"
      SubnetRange:
        default: "Input Subnet Range"
      EnvType:
        default: "Select Environment"
  '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
  Fn::Transform:
    Name: AWS::Include
    Parameters:
      Location: "ec2.yaml"
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>


以下`package`コマンドを実行することで、テンプレートファイルの変換とインクルードファイルのS3バケットへのアップロードが行われます。

また、テンプレートファイルの変換は、テンプレートファイルとして指定したファイルが直接変換されるのではなく、`--output-template-file`で指定したファイルに変換後の内容が出力されます。（今回は出力後のテンプレートファイルを`cf.yaml`とします。）

尚、インクルードファイルがアップロードされるS3バケットは予め作成しておいてください。

```shell:packageコマンドによる変換
aws cloudformation package --template-file cloudformation.yaml --s3-bucket [S3バケット名] --output-template-file cf.yaml
```

## 完成したテンプレートの実行
変換後のテンプレートファイルは以下となります。

`Transform`の`Location`だけではなく、yamlの`ブロックスタイル`への変更が行われるようです。

<details><summary>変換されたテンプレートファイル（展開して下さい）</summary><div>

```yaml:/home/ec2-user/cf.yaml
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Network Configuration
      Parameters:
      - VPCRange
      - SubnetRange
    - Label:
        default: EC2 Configuration
      Parameters:
      - EnvType
    ParameterLabels:
      VPCRange:
        default: Input VPC Range
      SubnetRange:
        default: Input Subnet Range
      EnvType:
        default: Select Environment
  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
  Fn::Transform:
    Name: AWS::Include
    Parameters:
      Location: s3://[S3バケット名]/[アップロードされたインクルードファイル名]
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>

このテンプレートを指定して`deploy`コマンドでスタックを作成していきますが、`deploy`コマンドは`create-stack`のコマンドのようにパラメータファイルを外部ファイルとして読み込むことができないため、パラメータの入力は`--parameter-overrides`コマンドで指定します。

```shell:deployコマンドによるスタック作成
aws cloudformation deploy --template-file cf.yaml --stack-name stack-test --parameter-overrides SubnetRange=172.24.0.0/24 VPCRange=172.24.0.0/16 EnvType=Production
```

# おわりに
今回は`Transform`によるファイルのインクルードを試しましたが、色々と制約があるためちょっと使いづらいと感じました。

今回の方法で`Transform`をバリバリ使うのであれば、普段から短縮形や括弧などを使う`フロースタイル`での構文は使わないよう心がければよさそうではありますが、複数のテンプレートを使用するやり方は`Transform`ではなく他の方法がいくつかあるようなので、そもそも`Transform`を使うべきではないのかもしれません。

次回は`変更セット`と`ドリフト検出`について勉強してみようと思います。