AWS SAM環境で複数環境にデプロイ + Secrets Managerで環境変数管理

# はじめに
サーバーレス環境のAWS Lambdaをローカルで開発できるよう、AWS SAMを使っています。
https://aws.amazon.com/jp/serverless/sam/

ソースコードもgit管理できるため便利ですし、ローカルで開発している間は課金される恐怖からも逃れることができます。
しかし、ローカルとそれ以外の環境は全く同じわけではありません。特に環境変数回りでハマることがあったので、記録しておきます。

# やりたいこと
- デプロイする環境に応じて環境変数を分けたい
- セキュアな情報はSecrets Managerで管理したい

# 結論
- 環境変数はtemplate.yamlに書く
- 環境変数の指定はsamconfig.tomlと実行時コマンドで行う
- Secrets Managerで保管した値を環境変数に指定し、template.yamlで読み込むことができる

# 前提
Windows 11 + WSL2でPython3環境のスクリプトを開発します。
WSLには`docker`, `sam`, `python3`を入れてあります。

また、AWS SAM自体の説明は省略します。

# AWS SAMでLambdaを開発する
## ローカル環境はDockerで開発する
ローカル環境は `sam local invoke`コマンドで実行します。
```shell
$ sudo sam build --use-container
$ sudo sam local invoke
```
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html#serverless-getting-started-hello-world-test

## 各環境はデプロイする
検証環境・本番環境にデプロイする際には、 `sam deploy` コマンドを使います。
```shell
$ sudo sam build --use-container
$ sam deploy --config-env staging
```
# 環境変数管理
前述のように、SAMを使うとローカルで開発できる代わりに、ローカル環境はDocker、それ以外の環境ではAWS環境になります。
ローカル環境は直書きでも問題ないですが、本番環境の環境変数をソースコード内に残したくないので、AWS Secrets Managerに記載します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/dynamic-references.html
## 環境に応じて設定を分ける

### 環境変数定義
SAMが使用する`template.yaml`に、環境変数一式を定義します。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-specification-template-anatomy.html


```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Environment:
      Variables:
        DatabaseHost: !FindInMap [ Variables, !Ref Env, DatabaseHost ]
        DatabasePort: !FindInMap [ Variables, !Ref Env, DatabasePort ]
        DatabaseUser: !FindInMap [ Variables, !Ref Env, DatabaseUser ]
        DatabasePassword: !FindInMap [ Variables, !Ref Env, DatabasePassword ]
        DatabaseName: !FindInMap [ Variables, !Ref Env, DatabaseName ]
...
Mappings:
  Variables:
    local:
      DatabaseHost: 'db'
      DatabasePort: '3306'
      DatabaseUser: 'hogehoge'
      DatabasePassword: 'example'
      DatabaseName: 'test'
    staging:
      DatabaseHost: '{{resolve:secretsmanager:staging-secret-id:SecretString:DatabaseHost}}'
      DatabasePort: '{{resolve:secretsmanager:staging-secret-id:SecretString:DatabasePort}}'
      DatabaseUser: '{{resolve:secretsmanager:staging-secret-id:SecretString:DatabaseUser}}'
      DatabasePassword: '{{resolve:secretsmanager:staging-secret-id:SecretString:DatabasePassword}}'
      DatabaseName: '{{resolve:secretsmanager:staging-secret-id:SecretString:DatabaseName}}'
  ...
Parameters:
  Env:
    Type: String
    Default: staging
    AllowedValues: [local, staging, production]
```

### 環境変数をSecrets Manager内に書く

![スクリーンショット 2023-11-24 151153.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9384/c505b559-c805-919e-5e0c-260da4aeac77.png)

https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/create_secret.html


### コマンドと環境変数をマッピングする
定義した環境変数を呼ぶには、コマンド内で`Env`指定を渡してあげる必要があります。
`samconfig.toml`各環境の`parameter_overrides` で、`template.yaml` の環境を指定します。
`[default]`や`[staging] `は、コマンドの `--config-env` 属性に対応します。

```shell
# samconfig.tomlの[staging]呼び出し
$ sam deploy --config-env staging
```

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html


```toml:samconfig.toml
version = 0.1

[default]
[default.global.parameters]
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
[default.package.parameters]
[default.sync.parameters]
watch = true
[default.local_start_api.parameters]
warm_containers = "EAGER"
[default.local_start_lambda.parameters]
warm_containers = "EAGER"
[default.local_invoke.parameters]
stack_name = "gate"
parameter_overrides = [
    "Env=local"
]

[staging]
[staging.deploy]
[staging.deploy.parameters]
stack_name = "gate-staging"
s3_bucket = "gate-staging-source"
s3_prefix = "staging"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
parameter_overrides = [
    "Env=staging",
]
```

### ソースコード内で参照する
Pythonだとこんな感じになります。

```python:app.py
import os
import mysql.connector
def lambda_handler(event, context):
  conn = mysql.connector.connect(
    host = os.environ['DatabaseHost'],
    port = os.environ['DatabasePort'],
    user = os.environ['DatabaseUser'],
    password = os.environ['DatabasePassword'],
    database = os.environ['DatabaseName'],
    connection_timeout = 3
  )
  # 以下、実装
```

# ハマったこと
## 複数環境にデプロイできない！
こんなコマンドを打っていたためでした。
```shell
$ sam deploy production
```
マニュアルを読むと、引数には必ずオプションが付くことがわかります。
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html
`production` とだけ書いても、コマンドは認識してくれず、defaultの `staging` をデプロイしようとします。
環境を指定するには、先述のsamconfig.tomlを記載した上で、
```shell
$ sam deploy --config-env production
```
と指定しなければいけません。