【CloudFormation】stackerを使って複数のスタックを管理する

[この記事で使ったコードとかはこちら](https://github.com/tsuttsu305/cfn-stacker-sample)

CloudFormation(以後CFn)で大規模なシステムを記述するときは、複数のテンプレートに分割して書いています。
分割するということは他のスタックの値を使用することがあるのですが、Cross Stack Referenceをしたくなかったので色々調べた結果Stackerにたどり着いた。

## 他のスタックに値を渡す方法 (復習?)
###  Cross Stack Reference
OutputsでExportし、読み込みたいテンプレートで!ImportValueする

```yaml
...
Outputs:
  VpcId:
    Value: !Ref Vpc
    Export:
      Name: VpcId
...
HogeFuga:
  Type: ...
  Properties:
    VpcId: !ImportValue VpcId
```

CFnの標準機能だけで実現可能
しかし、誰かがリソースをImportValueしていると、Export側のスタックが更新できない -> ボツの理由

### Nested Stack
他のスタックの値を親スタックから参照して、子スタックに渡す感じ。
ただ、一部スタックのみ更新ができない(関係ないスタックも処理が走ってしまう)&更新セットが子スタックは作成されないので使わなかった

### AWS CDK
[AWS クラウド開発キット](https://aws.amazon.com/jp/cdk/)
TypeScriptなどで記述できる。
複数のスタックを利用したインフラも作成可能。
しかし、内部ではCross Stack Referenceを使っていたのでボツに

### ShellScriptとAWS CLIで頑張る
今まではこれでやっていた。
そこそこいい感じに分割は可能。
しかし、テンプレートが増えるごとにShellScriptが増えて管理できなくなってきた。

## stacker
[stacker GitHub](https://github.com/cloudtools/stacker)
複数のStack設定をyamlで記述して、コマンド一発で展開可能なツール
Cross Stack Referenceを使用せずに、パラメータの受け渡しが可能っぽい。
テンプレート自体も[troposphere](https://github.com/cloudtools/troposphere)で作成したものが使えるので、ロジックが記述可能

troposphereのスターが3.9kあるのに、stackerは579しかないのはなんでだろうね。。。

### ディレクトリ構成
```
.
├── Pipfile ... Pipenv使ってるので
├── Pipfile.lock
├── blueprints ... troposphere + stacker blueprintを組み合わせたスクリプトを配置
│   ├── __init__.py
│   ├── instance.py
│   └── vpc.py
├── conf ... 環境ごとの設定ファイル。デプロイ時に指定可能
│   └── dev.env
├── stacker.yaml ... メインの設定ファイル
└── tasks.py ... TaskRunnerにInvokeを使用しているので (べつにMakefileでもいいと思う)
```

### stacker.yaml
```yaml
  
---
namespace: ${namespace} # CFnスタックの名前に使われる。実際の値はconf/dev.envに記載

stacker_bucket: stacker-tpl-${namespace} # CFn TemplateがUploadされるS3 Bucket名

sys_path: .

stacks:
  - name: vpc # 実際に作成されるスタック名は「${namespace}-vpc」になります
    class_path: blueprints.vpc.Vpc # BluePrintのClass名までを記載
    variables:
      CidrBlock: '10.0.0.0/16'
      # List表記にしたものはBlueprintでfor...inにして使う
      PublicSubnets:
        - '10.0.0.0/24'
        - '10.0.1.0/24'
      PrivateSubnets:
        - '10.0.128.0/24'
        - '10.0.129.0/24'
  - name: instance
    class_path: blueprints.instance.Instance
    variables:
      SubnetId: ${output vpc::PublicSubnet0} # 他のStackのOutputを取得する
      ImageId: ami-0c3fd0f5d33134a76
```

### blueprints/vpc.py
[troposphere_mate](https://github.com/MacHu-GWU/troposphere_mate-project)はtroposphereに型定義などが追加されてIDEの入力支援が色々使えるやつです。

```python
from stacker.blueprints.base import Blueprint
from troposphere import Ref
from troposphere_mate import Output
import troposphere_mate.ec2 as ec2


class Vpc(Blueprint):
    # stacker.yamlのvariablesに記載する内容の定義
    VARIABLES = {
        "CidrBlock": {
            "type": str,
            "description": "Vpc CidrBlock"
        },
        "PublicSubnets": {
            "type": list,
            "description": "Public Subnet CidrBlock List"
        },
        "PrivateSubnets": {
            "type": list,
            "description": "Private Subnet CidrBlock List"
        }
    }

    def create_template(self):
        var = self.get_variables()

        self.template.description = "VPC Stack"

        vpc = self.template.add_resource(
            ec2.VPC(
                'Vpc',
                CidrBlock=var['CidrBlock'],
                EnableDnsHostnames=True,
                EnableDnsSupport=True
            )
        )
        self.template.add_output(Output('VpcId', Ref(vpc)))

        internet_gateway = self.template.add_resource(ec2.InternetGateway('Igw'))

        self.template.add_resource(
            ec2.VPCGatewayAttachment(
                'VpcGwAttachment',
                VpcId=Ref(vpc),
                InternetGatewayId=Ref(internet_gateway)
            )
        )

        default_route_table = self.template.add_resource(
            ec2.RouteTable(
                'DefaultRouteTable',
                VpcId=Ref(vpc)
            )
        )

        self.template.add_resource(
            ec2.Route(
                'DefaultRoute',
                RouteTableId=Ref(default_route_table),
                DestinationCidrBlock='0.0.0.0/0',
                GatewayId=Ref(internet_gateway)
            )
        )

        # Create Public Subnet
        for k, v in enumerate(var['PublicSubnets']): # stacker.yamlでList表記にしていたものを使用
            public_subnet = self.template.add_resource(
                ec2.Subnet(
                    f'PublicSubnet{k}',
                    CidrBlock=v,
                    VpcId=Ref(vpc)
                )
            )

            self.template.add_output(Output(f'PublicSubnet{k}', Ref(public_subnet)))

            self.template.add_resource(
                ec2.SubnetRouteTableAssociation(
                    f'PublicSubnetRouteTableAssociation{k}',
                    RouteTableId=Ref(default_route_table),
                    SubnetId=Ref(public_subnet)
                )
            )

        # Create Private Subnet
        for k, v in enumerate(var['PrivateSubnets']):
            private_subnet = self.template.add_resource(
                ec2.Subnet(
                    f'PrivateSubnet{k}',
                    CidrBlock=v,
                    VpcId=Ref(vpc)
                )
            )
            self.template.add_output(Output(f'PrivateSubnet{k}', Ref(private_subnet)))
```

### デプロイ
```bash
$ stacker build --region ap-northeast-1 conf/dev.env stacker.yaml
```

### 削除
```bash
# 削除対象を確認
$ stacker destroy --region ap-northeast-1 conf/dev.env stacker.yaml
[2019-12-18T15:36:30] Using default AWS provider mode
[2019-12-18T15:36:30] Plan "Destroy stacks":
[2019-12-18T15:36:30]   - step: 1: target: "instance", action: "_destroy_stack"
[2019-12-18T15:36:30]   - step: 2: target: "vpc", action: "_destroy_stack"
[2019-12-18T15:36:30] To execute this plan, run with "--force" flag.

# 削除実行
$ stacker destroy --region ap-northeast-1 conf/dev.env stacker.yaml --force
```

## まとめ
複数のスタックの設定を一箇所で管理可能で、環境ごとの差分は.envとして管理ができるので非常に楽。
他スタックの値のImportがImportValueではなく、aws cliで取得した値を貼り付けているような感じなので参照先のリソースの更新も可能。
troposphereで記述できるため、開発環境だけ展開するインスタンスなども設定可能