cfn-execで簡単にCloudfomation Stackを作成・更新する

# はじめに
Cloudfomationを普段から開発し何度もスタック作成を試行錯誤している人で
TeraformやCDKへの移行ができない人向け

特にスタックをポチポチマネジメントコンソールでやっていたり、aws cliから作成するにもコマンドを複数叩いて回るのがメンドクサイ人におすすめ

# TL;DL
[cfn-exec](https://github.com/Takenori-Kusaka/cfn-exec)を利用する

# 前提
aws cli実行環境
python3.7以上の実行環境

note: OS依存性はない
note: aws-vault環境の場合は以下のように実行する

```sh
aws-vault exec yourprofile-- cfn-exec -v
```

# 詳細
| Command Line                            | Option                           | Description                                                                                                                                                                                                                 |
|-----------------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -n, --stack-name                        | STACK_NAME                       | 任意のスタック名(max 64文字)                                                                                                                                                                                                |
| -i, --input-path                        | INPUT_PATH                       | Cloudfomationファイルのローカル相対/絶対パス、またはS3バケットURL                                                                                                                                                                     |
| -p, --parameter-file                    | PARAM                            | Parameterファイルのローカル相対/絶対パス、またはパブリックなURLやS3バケットURL                                                                                                                                                                         |
| -s3, --s3-bucket-url-parameter-key-name *1 | S3_BUCKET_URL_PARAMETER_KEY_NAME | 指定するCloudfomationファイルのパスがローカルであり、かつそのCloudfomationファイルから別のCloudfomationファイルを参照するネスト構造を有する場合に、その参照先を指定するベースURLパスを定義するCloudfomationパラメータキー名 |
| -dr, --disable-roleback                 |                                  | Stack作成に失敗したとき、ロールバックしない                                                                                                                                                                                 |
| -del, --delete-stack                    |                                  | Stack作成の成否に関わらず実行後に削除する                                                                                                                                                                                   |
| -csf, --change-set-force-deploy         |                                  | Change set作成後にそのまま適用する                                                                                                                                                                                          |

*1: 説明が意味不明と思うので、[2.実行例 2.3. ローカルにあるネスト構造を持つCloudfomationテンプレートからスタックを作成する](#2.3) を参考すること

パラメータファイルはyaml or jsonフォーマットに対応し、aws cliフォーマットとシンプルフォーマットに対応している
aws cli フォーマット

```json
[
  {
    "ParameterKey": "BucketName",
    "ParameterValue": "cfnexec-example-test-01234567890123456789"
  }
]
```

```yaml
---
- ParameterKey: BucketName
  ParameterValue: cfnexec-example-test-01234567890123456789
```
シンプルフォーマット
```json
{
    "BucketName": "cfnexec-example-test-01234567890123456789"
}
```

```yaml
---
BucketName: cfnexec-example-test-01234567890123456789
```

## 1. インストール

```sh
pip install cfnexec
cfn-exec -v
```

## 2. 実行例

### 2.1 実行例の対象となるファイル

単体で実行可能なCloudformationテンプレート s3.yml

```yaml
---
AWSTemplateFormatVersion: "2010-09-09"
Description: s3.yml
Parameters:
  BucketName:
    Type: String
    Default: "BucketName"

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
```

s3.ymlを呼び出すCloudfomationテンプレート main.yml

```yaml
---
AWSTemplateFormatVersion: "2010-09-09"
Description: main.yml
Parameters: 
  TemplateS3BucketURL:
    Description: Referenced S3 bucket URL
    Type: String
    Default: TemplateS3BucketURL
  BucketName:
    Type: String
    Default: BucketName

Resources:
  # Create S3 Bucket
  S3:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub ${TemplateS3BucketURL}/components/s3.yml
      Parameters:
        BucketName: !Ref BucketName
```

s3.yml単体を実行するためのパラメータファイル

```yaml
---
- ParameterKey: BucketName
  ParameterValue: cfnexec-example-test-01234567890123456789
```

ネスト構造を持つmain.ymlを実行するためのパラメータファイル ローカルファイル版

```yaml
---
TemplateS3BucketURL: TemplateS3BucketURL
BucketName: cfnexec-example-test-01234567890123456789
```

ネスト構造を持つmain.ymlを実行するためのパラメータファイル GithubURL版

```json
{
    "TemplateS3BucketURL": "https://raw.githubusercontent.com/Takenori-Kusaka/cfn-exec/main/example/input/",
    "BucketName": "cfnexec-example-test-01234567890123456789"
}
```

なお、ローカルファイルの相対パス参照例はcfn-execをcloneし、そのルートディレクトリ上で実行したものである

```sh
git clone https://github.com/Takenori-Kusaka/cfn-exec.git
cd cfn-exec
```

### 2.2 ローカルにある単体実行可能なCloudfomationテンプレートからスタックを作成する

```sh
$ cfn-exec -n local-file1 -i ./example/input/components/s3.yml -p ./example/param_cfn.json
Found credentials in environment variables.
StackName: local-file1
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-9178840d-1214-43a4-9c8f-47fbd81d8f3c/s3.yml
Parameters:

  Index  ParameterKey    ParameterValue
-------  --------------  -----------------------------------------
      0  BucketName      cfnexec-example-test-01234567890123456789

Create a new local-file1.
Creating to stack... : local-file1

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId                         ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  -----------------------------------------  ----------------------
      0  2022-03-25 14:03:36.757000+00:00  CREATE_COMPLETE   S3Bucket             cfnexec-example-test-01234567890123456789

Creation to stack completed successfully!! : local-file1
```

<a id="2.3"></a>
### 2.3 ローカルにあるネスト構造を持つCloudfomationテンプレートからスタックを作成する

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml -s3 TemplateS3BucketURL
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-36e9fce4-388a-41d1-a11a-277026d25d9b/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  --------------------------------------------------------------------------------
      0  TemplateS3BucketURL  https://cfn-exec-us-east-2-36e9fce4-388a-41d1-a11a-277026d25d9b.s3.amazonaws.com
      1  BucketName           cfnexec-example-test-01234567890123456789

Create a new local-file2.
Creating to stack... : local-file2

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId                         ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  -----------------------------------------  ----------------------
      0  2022-03-25 14:10:48.058000+00:00  CREATE_COMPLETE   S3Bucket             cfnexec-example-test-01234567890123456789

Creation to stack completed successfully!! : local-file2
```

note: スタック作成完了後テーブルにはネスト先を含めて実際のAWSリソースのみを表示する

#### 動作フロー
ローカルファイルは一度S3へアップロードし、その上で対象のURLを指定する  
このアップロード時に対象ファイルと同じフォルダ下にあるものを同時にアップロードすることでURLを解決する
以下のファイル構造を持ち、Cloudfomationテンプレートにmain.ymlを指定した場合のフローを説明する

```
./example
│  param.yml
│
└─input
    │  main.yml
    │
    └─components
            s3.yml
```

1. 一時的なs3バケットを作成する(スタック名＋リージョン＋UUID)
1. main.ymlの親ディレクトリ(./example/input)を取得する
1. 2.のフォルダ以下にあるすべてのファイル(main.yml, components/s3.yml)をs3へアップロードする
1. s3バケットのURLとアップロードした対象のCloudfomationテンプレートファイルのURLを取得する
1. パラメータからs3バケットURLを指すキー(**-s3オプションで指定した名前**)を探し、その値を取得したs3バケットURLへ置換する
1. 対象のCloudfomationテンプレートファイルのURLと置換済みパラメータを利用してスタックを作成する
1. スタック作成完了後に一時的なs3バケットは削除する

3.の時点でS3バケット上のファイル構造は以下の通り

```
S3Bucket
│  main.yml
│
└─components
        s3.yml
```

### 2.4 インターネット上にある単体実行可能なCloudfomationテンプレートからスタックを作成する

```sh
$ cfn-exec -n public-file1 -i https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input/components/s3.json -p https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/param_cfn.json
Found credentials in environment variables.
StackName: public-file1
CFn URL: https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input/components/s3.json
Parameters:

  Index  ParameterKey    ParameterValue
-------  --------------  -----------------------------------------
      0  BucketName      cfnexec-example-test-01234567890123456789

Create a new public-file1.
Creating to stack... : public-file1

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId                         ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  -----------------------------------------  ----------------------
      0  2022-03-25 14:40:38.038000+00:00  CREATE_COMPLETE   S3Bucket             cfnexec-example-test-01234567890123456789

Creation to stack completed successfully!! : public-file1
```


### 2.5 インターネット上にあるネスト構造を持つCloudfomationテンプレートからスタックを作成する

```sh
$ cfn-exec -n public-file2 -i https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input/main.json -p https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/param.json
Found credentials in environment variables.
StackName: public-file2
CFn URL: https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input/main.json
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  ------------------------------------------------------------------------------------------------
      0  TemplateS3BucketURL  https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input
      1  BucketName           cfnexec-example-test-01234567890123456789

Create a new public-file2.
Creating to stack... : public-file2

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId                         ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  -----------------------------------------  ----------------------
      0  2022-03-25 14:49:55.072000+00:00  CREATE_COMPLETE   S3Bucket             cfnexec-example-test-01234567890123456789

Creation to stack completed successfully!! : public-file2
```

### 2.6 実行可能な組み合わせパターン

テンプレートはS3バケット、パラメータはローカルファイル

```sh
$ cfn-exec -n public-file1 -i https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/input/components/s3.json -p ./example/param.yml
```

テンプレートはローカルファイル、パラメータはs3バケットファイル

```sh
$ cfn-exec -n public-file2 -i ./example/input/main.yml -p https://cfn-exec-us-east-2-f0f165a5-f221-4b2f-9d69-34cb268d8c11.s3.us-east-2.amazonaws.com/param.json
```

テンプレートはローカルファイル、パラメータはGithubファイル

```sh
$ cfn-exec -n public-file2 -i ./example/input/main.yml -p https://raw.githubusercontent.com/Takenori-Kusaka/cfn-exec/main/example/param.json
```

上記の通り組み合わせパターンに依存性なし

### 2.7 スタック作成時にロールバックさせない

-drオプションを有効化する

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml -dr
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-ec4802c9-6c62-4868-af79-25a05289640c/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  -----------------------------------------
      0  TemplateS3BucketURL  TemplateS3BucketURL
      1  BucketName           cfnexec-example-test-01234567890123456789

Create a new local-file2.
Creating to stack... : local-file2
Waiter StackCreateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "CREATE_FAILED" at least once

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId    ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  --------------------  ------------------------------------
      0  2022-03-25 15:02:44.266000+00:00  CREATE_FAILED     S3                                         TemplateURL must be a supported URL.

Attempted to create but failed... : local-file2
```

"we matched expected path: **"CREATE_FAILED"** at least once"のCREATE_FAILEDが-drがないとき、ROLLBACK_COMPLETEになる  
自動的にロールバックした場合上記例のように失敗理由を確認することができない

### 2.8 スタック検証目的として、作成後に削除する(成否関係なく)

-delオプションを有効化する

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml -del
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-5087db33-a8d9-4754-853a-5aa2de2cdf21/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  -----------------------------------------
      0  TemplateS3BucketURL  TemplateS3BucketURL
      1  BucketName           cfnexec-example-test-01234567890123456789

Create a new local-file2.
Creating to stack... : local-file2
Waiter StackCreateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "ROLLBACK_COMPLETE" at least once

  Index  Timestamp                         ResourceStatus    LogicalResourceId    PhysicalResourceId    ResourceStatusReason
-------  --------------------------------  ----------------  -------------------  --------------------  ----------------------
      0  2022-03-25 15:06:01.605000+00:00  DELETE_COMPLETE   S3

Attempted to create but failed... : local-file2
Deleting to stack... : local-file2
Deletion to stack completed. : local-file2
```

最後にDeleteしている

### 2.9 変更セットを作成する

[2.実行例 2.3. ローカルにあるネスト構造を持つCloudfomationテンプレートからスタックを作成する](#2.3) を実行済みとし、すでにlocal-file2のスタックが存在する状態で実行する  
ここでは参照するパラメータのバケット名を変更して実行した  

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml -s3 TemplateS3BucketURL
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-c7d9d1bd-9788-4caf-82c1-12f8edfc9b4e/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  --------------------------------------------------------------------------------
      0  TemplateS3BucketURL  https://cfn-exec-us-east-2-c7d9d1bd-9788-4caf-82c1-12f8edfc9b4e.s3.amazonaws.com
      1  BucketName           cfnexec-example2-test-01234567890123456789

Since local-file2 already exists, create new change set.
Creating to change set... : local-file2-9bb145aa-0f20-445b-9d53-2b4fca411c19
Creation to change set completed successfully!! : local-file2-9bb145aa-0f20-445b-9d53-2b4fca411c19
Creation to change set completed successfully!! : local-file2-9bb145aa-0f20-445b-9d53-2b4fca411c19

  Index  Type      Action    LogicalResourceId    PhysicalResourceId                         ResourceType     Replacement
-------  --------  --------  -------------------  -----------------------------------------  ---------------  -------------
      0  Resource  Modify    S3Bucket             cfnexec-example-test-01234567890123456789  AWS::S3::Bucket  True
```

変更セットを作成したいスタック名、変更後のCloudfomationテンプレート、パラメータファイルを指定して実行する  
そのため、スタック作成時と全く同じコマンドで実行可能であり、ファイルの変更だけで対応可能(CIへ組み込み容易)  
テーブル出力から、変更差分を確認できる  

### 2.10 変更セット作成後そのまま適用する

2.9同様に[2.実行例 2.3. ローカルにあるネスト構造を持つCloudfomationテンプレートからスタックを作成する](#2.3) を実行済みとし、すでにlocal-file2のスタックが存在する状態で実行する  
追加のオプションとして-csfを有効化する  

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml -s3 TemplateS3BucketURL -csf
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-6c1dae89-1791-4acd-85ef-49fb48fe188f/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  --------------------------------------------------------------------------------
      0  TemplateS3BucketURL  https://cfn-exec-us-east-2-6c1dae89-1791-4acd-85ef-49fb48fe188f.s3.amazonaws.com
      1  BucketName           cfnexec-example2-test-01234567890123456789

Since local-file2 already exists, create new change set.
Creating to change set... : local-file2-eb08e8bf-6760-4b0e-8d35-0d23d21e8412
Creation to change set completed successfully!! : local-file2-eb08e8bf-6760-4b0e-8d35-0d23d21e8412
Creation to change set completed successfully!! : local-file2-eb08e8bf-6760-4b0e-8d35-0d23d21e8412

  Index  Type      Action    LogicalResourceId    PhysicalResourceId                         ResourceType     Replacement
-------  --------  --------  -------------------  -----------------------------------------  ---------------  -------------
      0  Resource  Modify    S3Bucket             cfnexec-example-test-01234567890123456789  AWS::S3::Bucket  True

Execute to change set: local-file2-eb08e8bf-6760-4b0e-8d35-0d23d21e8412
Executing change set...
Execution to change set completed successfully!! : local-file2-eb08e8bf-6760-4b0e-8d35-0d23d21e8412
```

最後にExecuteし適用している

### 2.11 (参考)変更セット作成失敗時の結果

```sh
$ cfn-exec -n local-file2 -i ./example/input/main.yml -p ./example/param.yml
Found credentials in environment variables.
StackName: local-file2
CFn URL: https://s3-us-east-2.amazonaws.com/cfn-exec-us-east-2-cfb30642-3946-4421-86cc-66b58fff76c9/main.yml
Parameters:

  Index  ParameterKey         ParameterValue
-------  -------------------  -----------------------------------------
      0  TemplateS3BucketURL  TemplateS3BucketURL
      1  BucketName           cfnexec-example-test-01234567890123456789

Since local-file2 already exists, create new change set.
Creating to change set... : local-file2-4dc75f4e-918c-4bd5-9606-32625c5107af
Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state: For expression "Status" we matched expected path: "FAILED"
Attempted to create but failed... : local-file2-4dc75f4e-918c-4bd5-9606-32625c5107af
Reason: TemplateURL must be a supported URL.
```

# おわりに
おそらく似たようなツールは世の中と各会社内に山ほどあると思うが、検索能力がなく見つけられなかったので作った  
誰かしらが助かることを祈って