CloudFormationのテンプレートを分割したくなりませんか？

# まえがき
AWS CloudFormationのテンプレートを書いていると長くなることがよくありますが、数千行を超えると読む気が失せると思います。
そんな長くなりがちなテンプレートを分割する方法を説明します。

# テンプレートの分割について

ネストされたスタック
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html

# テンプレート
IAMユーザーを作成するテンプレートを参考に分割します。

## 分割前
``` yaml:iam_user_create.yaml
Parameters:
  # パラメータ名がUserNameParameter　データ型がString
  UserNameParameter: 
    Type: String
  TagNames: 
    Type: String
Resources:
  MyIAMUser:
    Type: 'AWS::IAM::User'
    Properties:
      # !RefでパラメータのUserNameParameterを参照
      UserName: !Ref UserNameParameter
      # !RefでパラメータのTagNamesを参照
      Tags: 
        - Key: "tagname01"
          Value: !Ref TagNames 
```

# 分割するための書式
`Resources`セクションに以下を指定してください。
- `Type`に`AWS::CloudFormation::Stack`
- `Properties`の`TemplateURL`に参照先のAmazon S3 バケットにあるテンプレート
``` yaml:例
Resources:
  SampleIam:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL:https://sample-s3bucket.s3.ap-northeast-1.amazonaws.com/prefix/NestTemplate.yaml
```
詳細はAWSドキュメントのCloudFormationユーザーガイドの[AWS::CloudFormation::Stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stack.html)に記載されています。

# 方法①参照先を絶対パスで記載する
参照先のS3 バケットにあるテンプレートを絶対パスで指定します。

## 準備

### テンプレートの分割
今回はIAMユーザー作成部分だけ子テンプレートに切り出す形で分割します。
あまり意味はないですが、親テンプレートにはタグ指定だけ残しています。
``` yaml:親テンプレート(iam_user_create_parent_001.yaml)
Parameters:
  # パラメータ名がUserNameParameter　データ型がString
  UserNameParameter: 
    Type: String
  TagNames: 
    Type: String
Resources:
  MyIAMUserSplit:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://sample-s3bucket.s3.ap-northeast-1.amazonaws.com/stack_sample/iam_user_create_child_001.yaml
      Parameters: 
        ChildParameter: !Ref UserNameParameter
      # !RefでパラメータのTagNamesを参照
      Tags: 
        - Key: "tagname01"
          Value: !Ref TagNames 
```

``` yaml:子テンプレート(iam_user_create_child_001.yaml)
Parameters:
  # 親から引き継いだパラメータ名がChildParameter　データ型がString
  ChildParameter: 
    Type: String
Resources:
  MyIAMUserSplit:
    Type: 'AWS::IAM::User'
    Properties:
      # !Refで親から引き継いだパラメータのChildParameterを参照
      UserName: !Ref ChildParameter

```

### S3に格納
分割したファイルをS3に格納します。
![スクリーンショット 2024-07-03 114622.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/e7fa69d2-63d5-d5e8-7df2-9167beacb467.png)

## 動作確認
AWSコンソールからスタックを作成します。スタック名は`parent-sample`にします。
- テンプレートの指定で、親テンプレートのurlを指定します
![スクリーンショット 2024-07-03 195518_edit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/2e0c04fe-f5d2-8d26-94ff-78890cc80609.png)
- 正常に`parent-sample`とネストされたスタックが作成されました。（IAMユーザーも作成されています。）
![スクリーンショット 2024-07-03 115140.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/c399ee8e-e8d5-70e8-babc-48d79e084827.png)

# 方法②参照先を相対パスで記載する
前述の方法でも分割可能ですが、ネストするファイルが多い場合、参照先のURLをすべて書き換えるのが煩わしくなります。
他にも本番環境や開発環境でAWSアカウントが異なる場合等も書き換えが手間です。

解決方法として`cloudformation package`を使う方法があります。

## 準備
この方法の場合、TemplateURLに記載する参照先は相対パスになります。先ほどと異なりS3への格納は不要です。
```:構成
.
┣ iam_user_create_parent_001.yaml
┗ iam_user_create_child_001.yaml
```

``` yaml:親テンプレート(iam_user_create_parent_001.yaml)
Parameters:
  UserNameParameter: 
    Type: String
  TagNames: 
    Type: String
Resources:
  MyIAMUserSplit:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./iam_user_create_child_001.yaml
      Parameters: 
        ChildParameter: !Ref UserNameParameter
      Tags: 
        - Key: "tagname01"
          Value: !Ref TagNames 
```

## コマンド実行
- `cloudformation package`コマンドから親テンプレートを作成します。
`aws cloudformation package --template-file {親テンプレート} --s3-bucket {アップロード先バケット名} --output-template-file {作成されるテンプレートファイル名}`
このコマンドでは以下のことが行われます。
    - 相対パスで指定した子テンプレートファイルがS3にアップロードされる。
    - S3にアップロードされた子テンプレートを参照する親テンプレートが作成される。
    ``` sh:コマンド
    aws cloudformation package --template-file iam_user_create_parent_001.yaml --s3-bucket sample-s3bucket --s3-prefix Nested_Stack --output-template-file packaged.yaml
    ```
:::note warn
`cloudformation package` コマンドで指定するテンプレートに日本語のコメントがあると、`'cp932' codec can't decode byte`のメッセージを理由に失敗します。
:::

## コマンド実行結果
実行結果のS3とテンプレートは以下の通りです。
### S3の状態
名前と拡張子が変更されていますが、格納されているファイルが子テンプレートになります。
![スクリーンショット 2024-07-04 010632.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/ac126e37-e0e2-1a19-d8d3-537cab6f5de1.png)

### テンプレート
`packaged.yaml`が作成されました。このテンプレートの`TemplateURL`がS3の子テンプレートになっています。
親テンプレート・子テンプレートともに記載が少し変更されていますが、内容に変わりはありません。

```yaml:親テンプレート(packaged.yaml)
Parameters:
  UserNameParameter:
    Type: String
  TagNames:
    Type: String
Resources:
  MyIAMUserSplit:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.ap-northeast-1.amazonaws.com/sample-s3bucket/Nested_Stack/373e20f4f658ea58c1fe32a145232a4c.template
      Parameters:
        ChildParameter:
          Ref: UserNameParameter
      Tags:
      - Key: tagname01
        Value:
          Ref: TagNames
```
```yaml:子テンプレート
Parameters:
  ChildParameter:
    Type: String
Resources:
  MyIAMUserSplit:
    Type: AWS::IAM::User
    Properties:
      UserName:
        Ref: ChildParameter
```

## 動作確認
AWSコンソールからスタックを作成します。スタック名は`parent-sample-Nested-Stack`にします。
- テンプレートソースの指定は、親テンプレートのファイルをアップロードします。
![スクリーンショット 2024-07-04 013224.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/5a20364f-e2c6-06b0-b396-67db41d9b907.png)

- スタック作成を実行すると正常にスタックが作られました。
![スクリーンショット 2024-07-03 115140.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562079/357946f6-90f1-8b15-f717-4418b7281632.png)


# あとがき
テンプレートの分割方法を忘れかけていたので備忘も兼ねてまとめてみました。