Sceptreを使ってスタック間の依存がある複数のCloudformationスタックを一括で更新する

# はじめに

この記事では、Cloudformationで複数スタックを一括で更新する方法について取り上げます。

AWSリソースのIaCをする際、CloudformationやTerraformなどで管理することが多いと思います。Terraformでは`terraform apply`で複数のテンプレートをデプロイできる一方、Cloudformationでは公式から複数スタックを一括で更新をするすべが用意されていません。

AWS CLIでは、`aws cloudformation deploy`などを使って単一スタックに対してchangesetの作成及び実行、もしくは直接更新をかけることができます。ただ、これを利用して単純に複数のテンプレートをループさせて順々に更新していくと問題が生じます。スタック間で共有している値に変更があったりする場合、依存しているスタックは更新する順番を考慮してchangesetの実行を行う、もしくはスタック変更を反映させる必要があります。


# Cloudformationのスタック間で値を共有する方法

スタック間で値を共有する方法としては、Nested stackを利用する方法もありますが、今回はCross-stack referenceのみを考えたいと思います。

Cross-stack referenceとは、共有する値をOutputsに書いて出力することで他テンプレートでも使用可能になります。参照したい場合は`Fn::ImportValue`と`Outputs`で出力した名前を書くことで参照できます。

Export（値やリソースを共有する側）
```yaml
Outputs:
    IAMRole:
        Value: !Ref IAMRole
        Export:
            Name: !Sub "${AWS::StackName}::IAMRole"
```

Import（値やリソースを参照する側）
```yaml
Parameters:
    ImportStackName:
        Type: "String"

Resources:
    IAMManagedPolicyCI:
        Type: 'AWS::IAM::ManagedPolicy'
        Properties:
          ...
          Roles:
            - Fn::ImportValue: !Sub '${ImportStackName}::IAMRole'

```


# 自作スクリプトを組むときの注意

最初に書いたように、スクリプトを組んで一括でスタックの変更をしようとすると更新の順番を考える必要があります。

例えば、参照元の値が更新される場合は参照元のスタックを更新したあと、その値を使用しているスタックを更新する必要があります。

一方、Outputsの削除と参照しているリソースもしくはスタックを削除する場合は、参照しているスタックを更新したあと、Outputsを含むスタックを更新する必要があります。

また、スタック間で共依存や循環参照の関係があった場合はエラーにする必要があるなど、いい感じに自動反映をしてくれるスクリプトを作成しようとすると考慮しないといけないケースが多いです。

# OSSツール

このようにスクリプトを組むのは骨が折れます。依存による更新の順番を考慮した上で変更の一括反映が可能なOSSツールがないか探してみた結果、いくつかはありました。

|ツール名  |URL  |スター数  |
|---|---|---|
|Sceptre  |https://github.com/Sceptre/sceptre  |1.4k |
|stacker  |https://github.com/cloudtools/stacker  |695 |
|CloudGenesis |https://github.com/LifeWay/CloudGenesis|35 |

この中でSceptreを今回使って上で書いた注意点が解消されているか試してみたいと思います。

# Sceptre

https://github.com/Sceptre/sceptre/

Sceptreを使うためには、CloudformationテンプレートとSceptre用のconfigファイルをテンプレート毎に用意する必要があります。

ディレクトリ構成例としては、以下のようになります。

```
.
├── config
│   ├── config.yaml
│   └── dev
│       ├── config.yaml
│       └── vpc.yaml
└── templates
    └── vpc.yaml
```

configファイルはリージョンやプロフィールなど共通設定ファイルであり、子の階層で親のconfigファイルと同じ設定があった場合、子の設定ファイルの値で上書きされる仕組みになっています。

テンプレートごとの設定ファイル（config/dev/vpc.yamlなど）では、テンプレートのパス、スタック名などが設定することができ、既存のスタックに使用することも可能です。

使い方としては、pipコマンドもしくはdockerでインストール可能で、あとはsceptreコマンドをconfigとtemplateディレクトリが存在するディレクトリで実行します。

sceptreコマンドは、`sceptre create/update/delete`とスタックの操作に関連するものがあるのですが、スタックの作成・更新時は`sceptre launch`を使うと、存在しないスタックを新しく作成して、既存のスタックの更新をするので便利です。

上のディレクトリ構成例の場合、以下のコマンドで`config/dev`ディレクトリ内で設定したテンプレートのデプロイがまとめてできます。

## 検証

Sceptreの挙動を確認するために、以下のようなディレクトリ構成で検証してみます。

```
.
├── config
│   ├── config.yaml
│   └── dev
│       ├── base.yaml
│       └── depends.yaml
└── templates 
    ├── base.yaml
    └── depends.yaml
```

ファイルの中身は以下。今回は簡単な検証のために`base.yaml`でS3バケットを用意して、`depends.yaml`で`base.yaml`のS3バケットの名前を使って別のS3バケットを作るのみのテンプレート構成です。

```yaml:config/config.yaml
project_code: test-sceptre
region: ap-northeast-1
```

```yaml:config/dev/base.yaml
template:
  path: base.yaml
  type: file
```

```yaml:config/dev/depends.yaml
template:
  path: depends.yaml
  type: file

parameters:
  BaseStackName: !stack_output dev/base.yaml::S3BucketName
```

```yaml:templates/base.yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: "sceptre-test-hoge-bucket"

Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: !Sub '${AWS::StackName}::S3BucketName'
```

```yaml:templates/depends.yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  BaseStackName:
    Type: String

Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub "${BaseStackName}-2"
```

それでは、スタックの作成、更新、削除と試してみたいと思います。

スタック作成
![Screen Shot 2022-12-23 at 12.05.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321483/d6636eda-4fa6-85a3-6731-c928f43bba14.png)

スタック更新（base.yamlのS3バケット名を変更）
![Screen Shot 2022-12-23 at 12.09.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321483/1fd4b96e-4cfe-348e-9e6e-8c5711ba07bc.png)

スタック更新に失敗した場合（既に存在している名前でS3バケットを作成した）
![Screen Shot 2022-12-23 at 12.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321483/f7c98b6d-0ab1-06a6-32d4-0a9e0b8a8ee2.png)

スタック間で共依存関係になった場合
![Screen Shot 2022-12-23 at 12.47.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321483/2d7fa881-3770-4111-1cf0-5256845f7202.png)


スタック削除
![Screen Shot 2022-12-23 at 12.10.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321483/86f9f8e8-eae4-8fca-978b-5b2598c1d2d7.png)

他のケースも試してみましたが、Sceptreはスタック依存を考慮してそれぞれデプロイしてくれそうです。

# まとめ

今回の検証で、Sceptreを使うことでCloudformationでもスタック間の依存関係を考慮しながら一括で複数のスタックの更新をできることがわかりました。

同様にプログラムを書くこともできるとは思うのですが、車輪の再発明になるのであれば既に公開されているOSSを使ったり、必要に応じてカスタマイズして使っていく方が良いかもしれませんね。