CloudFormationをゼロから勉強する。（その８：スタックのネスト）

# はじめに
本記事でも何度か紹介している`AWS Black Belt Online Seminar`ですが、また新しいセミナーが公開されたようです。

- [AWS Black Belt Online Seminar AWS CloudFormation deep dive](https://www.slideshare.net/AmazonWebServicesJapan/20201006-aws-black-belt-online-seminar-aws-cloudformation-deep-dive)

3か月くらい前にも[CloudFormationのセミナー](https://www.slideshare.net/AmazonWebServicesJapan/20200826-aws-black-belt-online-seminar-aws-cloudformation-238501102)が公開されていましたが、それだけ`CloudFormation`の需要があるということなんでしょうね。

---

[その６](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)では`Transform`を使用した他テンプレートのインクルードを試しましたが、今回はスタックを`ネスト構成`にしてテンプレートを分割してみようと思います。

- 【前】[CloudFormationをゼロから勉強する。（その７：変更セットとドリフト検出）](https://qiita.com/sakai00kou/items/5a7ebb0898ea3eab7239)
- 【次】[CloudFormationをゼロから勉強する。（その９：クロススタックリファレンス）](https://qiita.com/sakai00kou/items/9512a8a271d19dad5886)
- 【番外編】[CloudFormationをゼロから勉強する。（番外編：AWS Perspective）](https://qiita.com/sakai00kou/items/b019de5266d81cae9971)
- 【番外編】[CloudFormationをゼロから勉強する。（番外編：Former2）](https://qiita.com/sakai00kou/items/9bdd23d18d38725d8ec8)

# ネストとは
イメージとしては[その６](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)の`Transform`でのテンプレート分割と同じような方法となりますが、`Transform`のインクルードはベーステンプレートの一部を別ファイルにして呼び出すイメージなので、実行単位であるスタックとしては**1スタック**であるのに対して、`ネスト`は他のスタックを呼び出すイメージとなるので、実行時の見え方は**複数スタック**となります。

共通コンポーネント用スタックを作成して、個々のスタックから呼び出すようにするのがAWSの[ベストプラクティス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html#nested)とのことです。

---

・・・とはいえ`Transform`のインクルードでも書き方次第で`ネスト`した場合と同じことができそうなのでいまいち区別がし辛いですが、`ネスト`の場合は[その６](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)で書いたようなyamlの書き方の制約は無いので、`Transform`のインクルードはリソースの一部パラメータ（例えばタグ）を分割したりするのに使い、`ネスト`はリソース自体を分割したりするのに使うといった区別が良いかと思います。

# ネストの書式
`Resources`セクションに`AWS::CloudFormation::Stack`タイプのリソースを作成することでネストしたスタックを指定できます。

ネストしたファイル名を指定する必要があるため、`Properties`で`TemplateURL`の指定が必須となります。

また、`S3バケット`へのアップロードが必要となるため、`Transform`でのインクルードと同様、実行する前に`package`コマンドでURLの変換、`S3バケット`へのアップロードを行う必要があります。


```yaml:ネストしたスタックの指定方法（抜粋）
Resources:
  NestStack1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./vpc.yaml
```

## ネストしたスタックへの入出力
ネストのスタックは親子関係となるため、`親スタック`から`子スタック`を呼び出す構成になります。

入出力の関係は`子スタック`を**プログラミング言語の関数**として考えるとイメージが湧きやすいと思いますが、`親スタック`で`子スタック`からの入出力を受ける場合、`子スタック`（という関数）に引数を渡すのが入力で、`子スタック`（という関数）からの戻り値が出力となります。

### 子スタック側での入出力書式
これはネストしていない場合と同じく`Parameters`セクションで上位からの入力を受けて、`Outputs`で結果を上位へ渡します。

### 親スタック側での入力書式
`親スタック`から`子スタック`へ値を入力するためにはネストするスタックの`Properties`に`Parameters`を記載して`子スタック`に渡す値を記載する必要があります。

上述の例で言うと、`親スタック`から`子スタック`（という関数）へ引数を渡す方法が、`Properties`の`Parameters`となります。

```yaml:子スタックへの入力値指定例
Resources:
  NestStack1:    ★子スタック名
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./vpc.yaml
      Parameters:
        NestVPCRange: !Ref VPCRange
        NestSubnetRange1: !Ref SubnetRange1
```

### 親スタック側での出力書式
`子スタック`（という関数）からの戻り値は`!GetAtt組み込み関数`で以下の書式で記載することで取得できます。

```yaml:出力値（戻り値）の取得
!GetAtt [子スタック名].Outputs.[子スタック側のOutputリソース名]
```

例えば`親スタック`で前項目の**子スタックへの入力値指定例**の設定、`子スタック`で以下のような出力設定を行ったとします。

```yaml:子スタック側でのOutput（抜粋）
Outputs:
  NestSubnet1Id:    ★Outputリソース名
    Value: !Ref EC2Subnet1
```

その場合、`子スタック`の出力値（戻り値）を取得するためには以下の指定を行うことで取得できます。

```yaml:親スタック側でのNestStack1出力値の取得例
!GetAtt NestStack1.Outputs.NestSubnet1Id
```

## ネストスタック間の入出力まとめ
ネストスタック間の入出力について、AWSマネジメントコンソール画面からの入出力も含めたイメージ図を描いてみました。

**青**が`子スタック`とのやり取り、**赤**が`自スタック`でのやり取りとなります。

![Stack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/29574fa0-cd15-d9db-02c4-13aafe213a40.png)

基本のイメージが分かれば、図のような複雑なスタック構成でも悩むことはないかと思いますが、注意点として、各スタック間の値の入出力は単純なネストの記述では**スタックを飛び越してやり取りできません。**

そのため、例えば図中の`NestStack2`の出力値を`NestStack3`で使用したい場合は、まず`NestStack1`の`Output`で`NestStack2`の戻り値を`Root Stack`に渡し、次に`Root Stack`から`NestStack3`の`Parameters`で`NestStack3`に値を引き渡す必要があります。

もし**スタックを飛び越してやり取りしたい**場合は、`Output`セクションで値を`Export`すればやり取りすることが可能なようです。
※`Export`する方法（`Cross Stack Reference`）は次回試そうと思います。

# 今回作成する構成
今回は以下のような構成を作ってみようと思います。

![Nest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/c79b941e-a207-5227-87c7-bd6164581dcc.png)

## ルートスタック用テンプレートの作成
各`子スタック`で使用する`VPCRange`、`SubnetRange1`、`SubnetRange2`を読み込み、各スタックの入力に引き渡します。

また、`vpc.yaml`で作成する`VPC ID`と`subnet1.yaml`で作成する`Subnet1のID`は`subnet2.yaml`と`ec2.yaml`で使用するため、`vpc.yaml`の出力値を`subnet2.yaml`と`ec2.yaml`の入力値として`!GetAtt`で指定するようにします。

```yaml:ルートスタック用テンプレート（cf.yaml）
AWSTemplateFormatVersion: 2010-09-09
Resources:
  NestStack1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./vpc.yaml
      Parameters:
        NestVPCRange: !Ref VPCRange
        NestSubnetRange1: !Ref SubnetRange1
  NestStack3:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./subnet2.yaml
      Parameters:
        NestVpcId: !GetAtt NestStack1.Outputs.NestVpcId
        NestSubnetRange2: !Ref SubnetRange2
  NestStack4:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./ec2.yaml
      Parameters:
        NestSubnetId: !GetAtt NestStack1.Outputs.NestSubnet1Id
Parameters:
  VPCRange:
    Type: String
    Description: "VPC Subnet Range"
  SubnetRange1:
    Type: String
    Description: "Subnet Range1"
  SubnetRange2:
    Type: String
    Description: "Subnet Range2"
```

## VPCスタック用テンプレートの作成
`VPC Range`と、`子スタック`となる`subnet1.yaml`の`Subnet1 Range`の情報を`Parameters`セクションで受け取り、`subnet1.yaml`に`Subnet1 Range`の値を渡します。

また、`Subnet1のID`は、`ec2.yaml`でも使用するため、`subnet1.yaml`の出力値を`vpc.yaml`の出力値として`ルートスタック`となる`cf.yaml`に渡すようにします。

```yaml:VPCスタック用テンプレート（vpc.yaml）
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NestVPCRange:
    Type: String
  NestSubnetRange1:
    Type: String
Resources:
  EC2VPC1:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref NestVPCRange
      Tags:
        - Key: "Name"
          Value: "cf_VPC1"
  NestStack2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./subnet1.yaml
      Parameters:
        NestVpcId: !Ref EC2VPC1
        NestSubnetRange1: !Ref NestSubnetRange1
Outputs:
  NestVpcId:
    Value: !Ref EC2VPC1
  NestSubnet1Id:
    Value: !GetAtt NestStack2.Outputs.NestSubnet1Id
```

## Subnet1スタック用テンプレートの作成
`親スタック`となる`vpc.yaml`からの値を`Parameters`で受け、`Subnet1のID`を`Outputs`で出力するようにします。

```yaml:Subnet1スタック用テンプレート（subnet1.yaml）
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NestVpcId:
    Type: 'AWS::EC2::VPC::Id'
  NestSubnetRange1:
    Type: String
Resources:
  EC2Subnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref NestVpcId
      CidrBlock: !Ref NestSubnetRange1
      Tags:
        - Key: "Name"
          Value: "cf_Subnet1"
Outputs:
  NestSubnet1Id:
    Value: !Ref EC2Subnet1
```

## Subnet2スタック用テンプレートの作成
`subnet2.yaml`は他スタックで使用する値は無いので、`親スタック`となる`cf.yaml`からの値を`Parameters`で受けるだけになります。

```yaml:Subnet2スタック用テンプレート（subnet2.yaml）
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NestVpcId:
    Type: 'AWS::EC2::VPC::Id'
  NestSubnetRange2:
    Type: String
Resources:
  EC2Subnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref NestVpcId
      CidrBlock: !Ref NestSubnetRange2
      Tags:
        - Key: "Name"
          Value: "cf_Subnet2"
```

## EC2スタック用テンプレートの作成
`subnet1.yaml`で出力した`Subnet1のID`を`Parameters`で読み込み、`EC2インスタンス`を作成します。

```yaml:EC2スタック用テンプレート（ec2.yaml）
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  NestSubnetId:
    Type: 'AWS::EC2::Subnet::Id'
Resources:
  EC2Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0cc75a8978fbbc969
      InstanceType: t2.micro
      KeyName: staging_key
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          SubnetId: !Ref NestSubnetId
          DeviceIndex: 0
```

# テンプレートの実行
`ネスト化`により、テンプレートが複数に分かれるため、[その６](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)と同様、`package`と`deploy`コマンドでテンプレートの変換と実行を行います。

URLなどのテンプレートの変換は、`ルートスタック`で行えば、`子スタック`も自動的に変換とS3への転送が行われるため、`ルートスタック用テンプレート`のみ実行します。

```shell:テンプレートの変換とS3バケットへの転送
aws cloudformation package --template-file cf.yaml --s3-bucket [S3バケット名] --output-template-file output.yaml
```

`package`コマンド実行後、`deploy`コマンドで`S3バケット`に転送したテンプレートを実行します。

```shell:テンプレートの実行
aws cloudformation deploy --template-file output.yaml --stack-name stack-test --parameter-overrides VPCRange=172.24.0.0/16 SubnetRange1=172.24.0.0/24 SubnetRange2=172.24.1.0/24
```

`CloudFormation`の`スタック`画面で以下の様になれば成功です。

![capture_18102020_142519.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/83984e1a-37a5-3db2-e34c-884ea15ebdda.jpeg)

# おわりに
`スタック`の`ネスト`は親子関係となるため、構造が分かりやすく、書き方とイメージを覚えてしまえば分かりやすいと感じました。

次も同じようなテンプレート分割方法となる`クロススタック参照`（`Cross Stack Reference`）を勉強してみようと思います。