AWS SAM CLI で複数のコンフィグを使い分ける

先日 AWS SAM CLI [v1.3.0 がリリース](https://github.com/aws/aws-sam-cli/releases/tag/v1.3.0)され、マルチコンフィグがサポートされました。
このアップデートにより、これまで面倒だった複数コンフィグの使い分けがとても楽になったので紹介してみます。

まず AWS SAM CLI の簡単なおさらいをしてから、アップデート前後でどう変わったかを見ていきます。

# AWS SAM と AWS SAM CLI

AWS SAM (Serverless Application Model) はサーバレスアプリケーション構築用のフレームワークです。
CloudFormation テンプレートを拡張する SAM 構文を使ってテンプレートを記述することで、簡単にアーキテクチャを定義できます。

例えば API Gateway と Lambda だけのシンプルなアプリケーションなら、たったこれだけの行数で書くことができます。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  SampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.lambda_handler
      Runtime: python3.7
      InlineCode: |
        import json

        def lambda_handler(event, context):
            return {
                'statusCode': 200,
                'body': json.dumps('Hello world!')
            }
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /helloworld
            Method: get
```

[AWS SAM CLI](https://github.com/aws/aws-sam-cli) は SAM ベースのアプリケーションの構築をサポートする CLI ツールで、AWS へのデプロイやローカルでの実行、テストなどをすることができます。

それでは SAM CLI を使って上記のアプリケーションをデプロイしてみましょう。
テンプレートを `template.yaml` という名前で保存したら、このコマンドを実行します。

```console
$ sam --version
SAM CLI, version 1.2.0
$ sam deploy --guided
```

いろいろ聞かれるので、答えていきましょう。

```
Stack Name [sam-app]: helloworld         # 任意のスタック名を入力
AWS Region [us-east-1]: us-west-2        # 任意のリージョンを入力
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]:
SampleFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to samconfig.toml [Y/n]:  # 設定をコンフィグファイルに保存するか

...

Deploy this changeset? [y/N]: y
```

CloudFormation コンソールを開くと、スタックが作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263249/155f54e5-d70a-081c-b279-4f9f4201a8ed.png)

作成した API を実行してみます。
以下のコマンドは、スタック名から API Gateway のリソース ID を調べてエンドポイント URL を組み立て、curl でリクエストしています。

```console
$ aws cloudformation describe-stack-resources \
--stack-name helloworld \
--query 'StackResources[?ResourceType==`AWS::ApiGateway::RestApi`].PhysicalResourceId' \
--output text \
 | xargs -IX curl https://X.execute-api.us-west-2.amazonaws.com/Prod/helloworld
"Hello world!"
```

ちゃんと動いていますね！
このように、SAM CLI を使うとサクッとアプリケーションを作ることができます。

ここでタイトルに関係してくるのが、先ほど `sam deploy --guided` を実行した際、**デプロイ時の設定をコンフィグファイル `samconfig.toml` に保存してくれる**ことです。
このファイルがあることで次回からは `sam deploy` だけでデプロイできるようになりますが、後述のとおり、これまでは少し使いづらい点がありました。

```toml
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "helloworld"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx"
s3_prefix = "helloworld"
region = "us-west-2"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
```

次に進む前に、デプロイしたリソースを消しておきます。

```
$ aws cloudformation delete-stack --stack-name helloworld
（出力なし）
```

# `--config-env` オプションが実装される前
ここから本題です。

SAM CLI を使うと `samconfig.toml` に設定を保存できますが、同じテンプレートでパラメータだけ変えて複数デプロイし、それぞれのコンフィグを保持しておく、ということを簡単に実現する方法がありませんでした。

これは、テンプレートファイルと同じ場所で `samconfig.toml` を探すことと、`samconfig.toml` にひとつの設定しか保存できないこと、`samconfig.toml` 以外の名前で保存できないことが原因でした。

そこで私は以下のようにディレクトリを分けることで複数のコンフィグ（ここでは例として開発用 dev と本番用 prod とします）を使い分けていました。

まず、共通の `template.yaml` と、環境別のディレクトリを用意しておきます。

```
app
├── dev
│   └── (empty)
├── prod
│   └── (empty)
└── template.yaml
```

デプロイ時は、`template.yaml` を各環境のディレクトリにコピーして `sam deploy` します。

```console
# dev デプロイ時
$ cp template.yaml app/dev
$ cd app/dev
$ sam deploy --guided

# prod デプロイ時
$ cp template.yaml app/prod
$ cd app/prod
$ sam deploy --guided
```

これにより各ディレクトリに `samconfig.toml` が作成されるので複数のコンフィグを使い分けられます。
ですが、もともとがシンプルだっただけに、少し複雑になったことですごく面倒に感じていました。

```
app
├── dev
│   └── samconfig.toml
├── prod
│   └── samconfig.toml
└── template.yaml
```

# `--config-env` オプションを使うと

前述の複数コンフィグの使い分けがとても簡単になります。

まずは SAM CLI を v1.3.0 にしておきます（以下は brew の例）。

```console
$ brew upgrade aws-sam-cli
$ sam --version
1.3.0
```

テンプレートファイルを用意します。

```
app
└── template.yaml
```

複数コンフィグを切り替えていることを確認できるように、先ほどとは少しだけ中身を変えておきます（`EnvType` の部分）。

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  EnvType:
    Type: String
Resources:
  SampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.lambda_handler
      Runtime: python3.7
      InlineCode: |
        import os
        import json

        def lambda_handler(event, context):
            return {
                'statusCode': 200,
                'body': json.dumps(os.environ.get('ENV_TYPE'))
            }
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /envtype
            Method: get
      Environment:
        Variables:
          ENV_TYPE: !Ref EnvType
```


`--config-env` オプションを指定してデプロイします。

```console
# dev デプロイ時
$ sam deploy --guided --config-env dev
```

先ほどと同じように質問されますが、最後の質問でコンフィグの環境名を指定できるように変わっています（オプションで指定した `dev` がデフォルト値になっている）。

```
Stack Name [sam-app]: sample-dev
...
Parameter EnvType []: dev                  # EnvType パラメータに env 名を入力しておきます

...（アップデート前と同じ質問）

SAM configuration file [samconfig.toml]:   # コンフィグの保存名
SAM configuration environment [dev]:       # コンフィグの環境名 ← こっちが重要
```

スタックを作成して保存された `samconfig.toml` を見てみると、先ほどは `[default]` だった部分が `[dev]` になっています。

```toml
version = 0.1
[dev]
[dev.deploy]
[dev.deploy.parameters]
stack_name = "sample-dev"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx"
s3_prefix = "sample-dev"
region = "us-west-2"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "EnvType=\"dev\""
```

続けて prod をデプロイします。

```
# prod デプロイ時
$ sam deploy --guided --config-env prod
```

```
Stack Name [sam-app]: sample-prod
...
Parameter EnvType []: prod                 # 今度は prod を入力しておきます

...（アップデート前と同じ質問）

SAM configuration file [samconfig.toml]:
SAM configuration environment [prod]:
```

コンフィグに `[prod]` セクションが追記されました。

```toml
version = 0.1
[dev]
[dev.deploy]
[dev.deploy.parameters]
stack_name = "sample-dev"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx"
s3_prefix = "sample-dev"
region = "us-west-2"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "EnvType=\"dev\""

[prod]
[prod.deploy]
[prod.deploy.parameters]
stack_name = "sample-prod"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx"
s3_prefix = "sample-prod"
region = "us-west-2"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "EnvType=\"prod\""
```

動作確認してみます。

```console
$ aws cloudformation describe-stack-resources \
--stack-name sample-dev \
--query 'StackResources[?ResourceType==`AWS::ApiGateway::RestApi`].PhysicalResourceId' \
--output text \
 | xargs -IX curl https://X.execute-api.us-west-2.amazonaws.com/Prod/envtype
"dev"

$ aws cloudformation describe-stack-resources \
--stack-name sample-prod \
--query 'StackResources[?ResourceType==`AWS::ApiGateway::RestApi`].PhysicalResourceId' \
--output text \
 | xargs -IX curl https://X.execute-api.us-west-2.amazonaws.com/Prod/envtype
"prod"
```

dev と prod がどちらもデプロイされていること、スタックパラメータで指定された `EnvType` が正常に反映されていることが確認できました。

最後に、スタックは削除しておきましょう。

```console
$ aws cloudformation delete-stack --stack-name sample-dev
$ aws cloudformation delete-stack --stack-name sample-prod
```

# 参考
- AWS Serverless Application Model (SAM) コマンドラインインターフェイス。サーバレスアプリケーションをローカルで構築、テスト、デバッグする: https://aws.amazon.com/jp/blogs/news/aws-serverless-application-model-sam-command-line-interface-build-test-and-debug-serverless-apps-locally/
- AWS SAM CLI: https://github.com/aws/aws-sam-cli
- Release 1.3.0: https://github.com/aws/aws-sam-cli/releases/tag/v1.3.0
- --config-env の Proposal: https://github.com/aws/aws-sam-cli/issues/1615