AWS CodeStarでネストスタックテンプレートを整理する

#目的
CodeStarで作成したプロジェクトについて、

- yamlファイルを分割し、
- そのファイルを整理し、
- かつ`buildspec.yml`の記述を短くする。
- その上で、テンプレートが増えてもできる限りコマンドが増えないようにする。


#まえがき
CodeStarを使うと、CodeBuildとCloudFormationとを使ってYamlファイルで[^1]AWSリソースを配置することができ、しかもそれらをgitで管理できる環境を自動構築してくれるため、非常に便利だ。しかし、リソースが増えていくと、どうしてもテンプレートファイルが長くなりやすく、可読性が下がってしまう。
そんなときは、スタックのネストを使うと便利だ。

しかし、(当然のことだが)yamlファイルを分割すればするほどファイル数が増えてしまい、保守しづらくなってしまうほか、`buildspec.yml`も冗長になってしまう。

```
＃このディレクトリは汚い
./--- ParentStack.yml
   |
   |- buildspec.yml
   |
   |- template-configration.json
   |
   |- templatefile1.yml
   |
   |- functionFolder1
   |
   |- templatefile2.yml
   |
   |- functionFolder
   |
   (以下略)
```

```yaml:buildspec.yml
#真面目にやろうとするとbuildspec.ymlはテンプレートが1つ増えるごとに2行ずつ増えていく。
  build:
    commands:
      # Use AWS SAM to package the application by using AWS CloudFormation
      #First, package child package
      - >-
        aws cloudformation package --templat template1.yml
        --s3-bucket $S3_BUCKET --output-template export-template1.yml 
      - >-
        aws cloudformation package --templat template1.yml
        --s3-bucket $S3_BUCKET --output-template export-template1.yml
      #Then, package parent stack
      - >-
        aws cloudformation package --template ParentStackFile.yml
        --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/ParentStackFile.yml 


      # Do not remove this statement. This command is required for AWS CodeStar projects.
      # Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources.
      - sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json
  
  post_build:
    commands:
      - mv template-configuration.json ./ExportFiles/
      - cd ExportFiles

artifacts:
  type: zip
  base-directory: ./ExportFiles
  files: 
    # This file is used code pipeline.
    - ParentStackFile.yml

    # They are child stackes output. 
    - export-stack1.yml
    - export-stack2.yml 

    #templates
    - template-configuration.json

```
そこで、yamlファイルを分割しつつそのファイルを整理し、かつ`buildspec.yml`の記述を短くしたい。

#結論
###ディレクトリ構造
以下のようなディレクトリ構造にする。
CodePipelineやCodeURIの指定等に応じて適宜変更すること。

```
./--- ParentStack.yml
   |
   |- ExportFiles---description.txt(このファイルはファイル名、ファイル形式を問わない)
   |
   |- buildspec.yml
   |
   |- template-configration.json
   |
   |- childTemplateFiles--- template1--- templatefile1.yml
                         |            |
                         |            |- functionFolder1(lambda関数のソースを含む場合)
                         |
                         |- template1--- templatefile2.yml
                         |            |
                         |            |- functionFolder2(lambda関数のソースを含む場合)
                         |
                        (以下略)
```

###親スタックのテンプレート
親スタックは、ネストするテンプレートについて以下のように記載する。

```yml:ParentStack.yml
Resources:
  #Nest stack
  Stack1:
    Description:
      This is test.
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: './ExportFiles/export-templatefile1.yml'
  
  Stack2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: './ExportFiles/export-templatefile2.yml'
      Parameters:
        ProjectId: !Ref ProjectId
        CodeDeployRole: !Ref CodeDeployRole
        Stage: !Ref Stage
```

###buildspecの記述
`buildspec.yml`を、以下のように記述する。

```yml:buildspec.yml
version: 0.2

phases:
  install:
    commands:
      
      # Upgrade AWS CLI to the latest version
      - pip install --upgrade awscli
      #First, pack child packages
      - cd childTemplateFiles
      - |、
        for file in `ls -1 ./*/*.yml`
        do
          echo "$file"
          filename=`basename $file`
          echo $filename
          aws cloudformation package --template $file --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/export-"$filename"
        done
      - cd ..
  build:
    commands:
      # Use AWS SAM to package the application by using AWS CloudFormation
      # This command replaces PATH to this build enviroment path
      - sed -i "s|PATH|$CODEBUILD_SRC_DIR|g" ParentStackFile.yml
      #Then, pack parent stack
      - >-
        aws cloudformation package --template ParentStackFile.yml
        --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/ParentStackFile.yml 

      # Do not remove this statement. This command is required for AWS CodeStar projects.
      # Update the AWS Partition, AWS Region, account ID and project ID in the project ARN on template-configuration.json file so AWS CloudFormation can tag project resources.
      - sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json
  
  post_build:
    commands:
      - mv template-configuration.json ./ExportFiles/

artifacts:
  type: zip
  base-directory: ./ExportFiles
  files: 
    - '*'

    
```

#試行錯誤の内容
##コマンドが長すぎ
まず始めたのが、特にこのコマンドが長すぎるのでどうにかして短くすること。

`aws cloudformation package --template ParentStackFile.yml --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/ParentStackFile.yml`

Yamlは改行の扱いが結構細かく決められている印象だったので探してみたらあった。[^2]

どうやら、`>-`とつければ良いらしい。これで長過ぎるコマンドが、次のように表記でき、見やすくなった。


```
#文頭に>-をつけることで、改行がスペースとして扱われる。
- >-
  aws cloudformation package --template ParentStackFile.yml
  --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/ParentStackFile.yml
```

ただ、`sed -i.bak 's/\$PARTITION\$/'${PARTITION}'/g;s/\$AWS_REGION\$/'${AWS_REGION}'/g;s/\$ACCOUNT_ID\$/'${ACCOUNT_ID}'/g;s/\$PROJECT_ID\$/'${PROJECT_ID}'/g' template-configuration.json`はどうにもうまく行かなかった。まあいいかと思い、次へ

##子スタックをまとめてパッケージしたい
###子スタックをforでビルド
スタックをネストするとき、CodeStarの初期設定だと概ね以下のような流れで行う。(他にも様々なフェーズがあるが、ここでは割愛)

- install
    - 最低限aws cliをアップデート。必要なら他のパッケージを入手
- build
    - `aws cloudformation package`で子スタック→親スタックの順にパッケージ。
- artifact
    - パッケージされたテンプレートを送信

スタックが一つのうちは良いのだが、テンプレートファイルが増えると`aws cloudformation package`コマンドをいくつも書かなくてはならず、非常に面倒だと思った。
ところで、テンプレートファイルはymlと決めていることだし、**forコマンドで.ymlファイルだけを検索し、ビルドできるのでは?と考えた。**
そこで、unixコマンドの`for`について調べたところ、[このサイト](https://shellscript.sunone.me/for.html)[^3]からちょうど良さそうなものが見つかったため、次のように書き換えてローカルで実行した。


```shell:test-for
for var in `ls -1 *.yml`
> do
>   echo $var
> done

```

```text:res
template1.yml
template2.yml
```
問題は、`buildspec.yml`内での記述だが、先程のページから[^2]次のように記載すれば良いことがわかった。[^4]

```yml:buildspec.yml
- cd childTemplateFiles #やらなくても良いが、気分で入れた
- |
        for file in `ls -1 ./*/*.yml` #cdしなかったらもう一つ階層を足す
        do
          echo "$file"
          filename=`basename $file`
          echo $filename
          #カレントディレクトリに出力する。
          aws cloudformation package --template $file --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/export-"$filename"
        done
```

###子スタックをまとめる
子スタックのテンプレートのみをまとめたフォルダを作り、そこにファイルをまとめた。
ビルドすると、lambda関数の`CodeURI`指定がうまく行かない。どうも、**テンプレートファイルをパッケージするとき、そのテンプレートファイルがあるフォルダがカレントディレクトリとして認識されるようだ。**
では、いっそのこと次のようにまとめてしまえばフォルダもスッキリする。

```
./--- ParentStack.yml
   |
   |- buildspec.yml
   |
   |- template-configration.json
   |
   |- childTemplateFiles--- template1--- templatefile1.yml
                         |            |
                         |            |- functionFolder1(lambda関数のソースを含む場合)
                         |
                         |- template1--- templatefile2.yml
                         |            |
                         |            |- functionFolder2(lambda関数のソースを含む場合)
                         |
                        (以下略)
```

###どうせならファイルの送信もforで回したい
これでも十分短くなったが、artifactフェーズで送信するテンプレートを指定しなければならないのは面倒だ。
そこで、Exportfileという空フォルダを作り、送信するファイルはそこにまとめることにした。
そこでbuildspecを以下のように編集して実行してみる。

```yml:buildspec.yml
- cd childTemplateFiles
- |
        for file in `ls -1 ./*/*.yml`
        do
          echo "$file"
          filename=`basename $file`
          echo $filename
          #ExportFileに出力する
          aws cloudformation package --template $file --s3-bucket $S3_BUCKET --output-template $CODEBUILD_SRC_DIR/ExportFiles/export-"$filename"
        done
- ls ./ExportFiles
```
ところが、このまま実行すると「フォルダがない」と言われる。lsしても見当たらないため、ビルド環境にフォルダが作成されていないようだ。
おそらく、S3はフォルダという概念がない[^5]ため、空フォルダを作るとS3上で`Null`に変換され、ビルド環境にフォルダが作成されないのではないかと考えられる。
なので、適当なファイルをExportFileにぶちこみ、もう一度実行すると`Successfully packaged artifacts and wrote output template to file `となった。

###親スタックの変更
親スタックは、テンプレートファイルを以下のように記述することで依存関係を表現できた。

```yml:ParentStack.yml
Resources:
  #Nest stack
  Stack1:
    Description:
      This is test.
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: './ExportFiles/export-templatefile1.yml'
  
  Stack2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: './ExportFiles/export-templatefile2.yml'
      Parameters:
        ProjectId: !Ref ProjectId
        CodeDeployRole: !Ref CodeDeployRole
        Stage: !Ref Stage
```

###ファイルの送信
最後に、送信するファイルを指定すれば完了だ。
最初は、以下のようなコマンドを試したが、全てダメだった。

```yml:buildspec_reject.yml
#試し
artifacts:
  type: zip
  files:
  zip:
    - |
        for file in `ls -1 ./ExportFiles/*.yml`
        do
          echo "$file"
          filename=`basename $file`
          echo $filename

#まあechoじゃだめだよね。filelistがrequiredだったのでls -1してみる
type: zip
files:
  - `ls -1`
#だめです。いっそ
type: zip
files:　.
#だめでした。
```
ここで、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html)[^6]を参照したところ、どうも次のようにシングルクオートで囲めば良いようだ。

```yml:buildspec_reject.yml
#試し
  
artifacts:
  type: zip
  files:
  zip:
    - ./ExportFiles/*.yml
```
なぜかファイルが送信できない。`post_build`なるフェーズがあるらしいので、ここで`cd`を使ってみる。

```yml:buildspec_reject.yml
#試し
post_build:
  - cd ./ExportFiles
  
artifacts:
  type: zip
  files:
  zip:
    - ./*.yml
```
ところが、これを実行したところ、次のようなレスポンスが返ってきた。

```text:res
[Container] 2019/03/24 10:20:34 Expanding ./*
[Container] 2019/03/24 10:20:34 Found 2 file(s)
```
どうも、カレントディレクトリを探索し、2つのymlファイル(ここではbuildspec.ymlとビルド前のParentStack.yml)を探し当てたようだ。

cdの代わりにbase-directoryを指定すると次のようになった。

```yml:buildspec_reject.yml
#試し
post_build:
  
artifacts:
  base-directory: Export
  type: zip
  files:
  zip:
    - ./*.yml
```

```text:res
[Container] 2019/03/24 10:25:18 Expanding ./ExportFiles/*
[Container] 2019/03/24 10:25:18 Found 10 file(s)
```
これで成功したかと思ったが、`template-configuration.json`も送信しなければならないため、コードを次のように変更したところ、成功した。

```yml:buildspec.yml
  post_build:
    commands:
      - mv template-configuration.json ./ExportFiles/
      - cd ExportFiles

artifacts:
  type: zip
  base-directory: ./ExportFiles
  files: 
    - '*'
```

[^1]: jsonでも記述できるが、非常に可読性が悪く書きづらい
[^2]: [プログラマーのための YAML 入門 (初級編)](https://magazine.rubyist.net/articles/0009/0009-YAML.html)
[^3]: [UNIX$Linuxコマンド・シェルスクリプトリファレンス](https://shellscript.sunone.me/for.html)
[^4]: どうでも良い情報かもしれないが、変数をバッククォートで囲むと`Permission Denied`という結果が返ってくる。どいういうことだろうか?
[^5]: [Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする](https://dev.classmethod.jp/cloud/aws/amazon-s3-folders/)
[^6]: https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html