AWS Lambdaで使いたい環境変数をAWS SAM CLIでどうするか

## はじめに
環境変数の扱いについて、戸惑いがありましたのでまとめます。
用途や仕様の背景が理解し切れて一ませんが、少なくとも現状はベストエフォートとは思えず、改善が進むと嬉しいです。

## やっていたことの概要
- 言語：Rust（記事としては、Rustに依存する内容ではないです）
- 作っていたもの：AWS Lambda関数
- 動作確認の方法：後述（※）
- デプロイの方法：AWS SAM CLI（`sam deploy`）
- その他：AWSの設定はCloudFormation（`template.yaml`）に集約

##### ※動作確認の方法
AWS SAMを使うとローカルでLambda関数の動作確認が可能ですが、私の環境ではビルドが遅くデプロイ直前までは、`cargo run`で動作確認を行っていました。

|  動作確認タイミング  |  動作確認の例  |  動作確認環境  |
| ---- | ---- | ---- |
|  開発時  |  `cargo run`, `cargo test`  |  MacOS  |
|  デプロイ前検証  |  `sam local invoke`  |  [カスタムランタイムのエミュレーションコンテナ](https://gallery.ecr.aws/sam/emulation-provided.al2)  |
|  デプロイ後検証  |  AWSコンソールからLambdaトリガー実行  |  [Lambda上のカスタムランタイムのコンテナ（Zipでのデプロイのため）](https://gallery.ecr.aws/lambda/provided)  |

ここで問題なのは、動作確認のタイミングで全ての環境が異なる点です。
Rustに限らず環境変数は`.env`ファイルを作成し何らかの手段でプログラム利用するのが多いかと思います。
ただデプロイ前検証、デプロイ後検証では、macに置いた`.env`ファイルは何もしなければ持ち込まれません。

なお、環境設定ファイルなので環境ごとに設定するものを記載すべきかもしれませんが、GitHubに上げたくない隠匿したい情報を記載しています。環境ごとの違いはなく同じファイルを読んで欲しいのに読んでくれない・・・という問題に直面しました。

## 解決策
いくつか試しました。

- [解決策①ビルド時に`.env`をコピー対象にする](#解決策ビルド時にenvをコピー対象にする)
- [解決策②`template.yaml`に環境変数をベタ書きする](#解決策templateyamlに環境変数をベタ書きする)
- [番外編`--env-vars`の利用](#番外編--env-varsの利用)
- [解決策③`--parameter-overrides`の利用](#解決策--parameter-overridesの利用)
- [解決策④`samconfig.toml`にパラメータを定義する](#解決策samconfigtomlにパラメータを定義する)


## sam local invokeで環境設定ファイルが読み込めない
まずは通常のRust開発を行なった状態から。
Rustにおける環境設定ファイルの利用は、`dotenv`、`once_cell`を使いました。

```:/work/project/.env
TEST=HelloEnv
AWS_S3_BUCKET=my-work-project-bucket
```
```rust:/work/project/src/config/config.rs
use serde::Deserialize;
use config::ConfigError;
use dotenv::dotenv;
use once_cell::sync::Lazy;

#[derive(Deserialize, Debug)]
pub struct Config {
    pub test: String,
    pub aws_s3_bucket: String,
}
// 初期化（定数へセット）
pub static CONFIG: Lazy<Config> = Lazy::new(|| 
    {
        dotenv().ok();
        Config::from_env().unwrap()
    }
);

// 環境変数を読み込む
impl Config {
    pub fn from_env() -> Result<Self, ConfigError> {
        let mut cfg = config::Config::new();
        cfg.merge(config::Environment::new())?;
        cfg.try_into()
    }
}

mod tests {
    use super::*;
    #[test]
    fn get_env() {
       assert_eq!(CONFIG.test, "HelloEnv");
   }
}
```
 `cargo`では、問題なく動作しています。

```bash:
$ cd /work/project
$ cargo test
test config::config::tests::get_env ... ok
```
しかし、`sam local invoke`では環境変数がなく構造体に展開できないエラーが出ます。

```bash:
$ cd /work
$ sam build
$ sam local invoke {関数名}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: missing field `test`', src/config/config.rs:15:28
```
このコマンドを実行しているのは、MacOS上ですが、`sam local invoke`ではコンテナが起動してその中で関数が実行されるため、`.env`が参照できていません。

## 解決策①ビルド時に`.env`をコピー対象にする
ビルドに利用しているMakefile内に1行追加。

```makefile:/work/project/Maikefile（抜粋）
build-StockRustFunction:
	cargo build --bin stock --release --target x86_64-unknown-linux-musl
	cp ./target/x86_64-unknown-linux-musl/release/stock $(ARTIFACTS_DIR)/bootstrap
	cp ./.env $(ARTIFACTS_DIR)/.env # ★★今回追加★★
```
デプロイ前動作確認にて問題ないことを確認

```bash:再ビルドが必要
$ cd /work
$ sam build
$ sam local invoke {関数名} // 成功
```
デプロイ後動作確認にて、環境変数という形で参照できないものの関数実行は成功することを確認。
![スクリーンショット 2021-09-29 14.18.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/0330ceb3-ba31-3466-a288-a162e692b5b4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/0f672060-5946-c640-0f5e-c3c28d656ac8.png)

これでいいんだ感。
いろいろ試しましたが、<b>開発環境とデプロイデプロイ環境で同一の設定情報を使う限りにおいては</b>、編集ファイルが少なく一番手っ取り早い手段でした。商用利用するサービスでは確実に開発と商用環境は異なると思いますのであくまで個人開発の範疇でしたら利用してもいいかもという感覚です。
Lambdaの「環境変数」で見えないため、デプロイ後時間が空いてしまうと設定していない情報がなぜか使えているという挙動が分かりづらい点も気になります。

## 解決策②`template.yaml`に環境変数をベタ書きする
`template.yaml`には、環境変数を定義する箇所があります。
これは、ビルド情報に含まれるためデプロイ前（`sam local invoke`）環境、デプロイ後（`sam deploy`）環境どちらでも有効に働きます。

```yaml:/work/template.yaml（抜粋）
Resources:
  StockRustFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment: 
        Variables:
          TEST: HelloEnv
          AWS_S3_BUCKET: my-work-project-bucket
```
デプロイ前動作確認にて問題ないことを確認

```bash:再ビルドが必要
$ cd /work
$ sam build
$ sam local invoke {関数名} // 成功
```
デプロイ後動作確認にて、環境変数が設定されていることを確認。
なお、この画面上の「編集」ボタンにて追加削除も可能なので、`template.yaml`を使わずともデプロイ後の関数に追加することも可能ですが、毎回手での追加作業が発生します。
![スクリーンショット 2021-09-29 13.51.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/63c329fd-6c33-a48b-31d8-d58f6f1861f8.png)

開発環境は、`.env`、デプロイ環境は`template.yaml`というわけで環境ごとにファイルが分けられているのも自然ですし、特に問題ない手段かと思いますが、`template.yaml`にベタ書きなので設定する環境変数をまとめて管理したい場合や秘匿情報は別管理したい場合に不向きです。

## 番外編`--env-vars`の利用
公式ドキュメントより、`sam local invoke`の環境変数の持ち込みに使えそうなオプションは、`--env-vars`と`--parameter-overrides`があります。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-invoke.html

まずは、[リンク先](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html#serverless-sam-cli-using-invoke-environment-file)に従い、`--env-vars`から試してみます。
![スクリーンショット 2021-09-29 13.12.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/95a4edde-a65d-10f0-c6d3-3d26371ba826.png)
なお、後述しますが、こちらは、`sam build`、`sam deploy`のオプションとしては存在していないため、最終的には利用をしていません。
`--env-vars PATH`のPATH先のファイルは、`json`のみ指定可能で、なんと`.env`は利用できないため、新たに作成します。

```json:/work/project/env.json
{
    "StockRustFunction": {
        "TEST": "HelloEnv!!",
        "AWS_S3_BUCKET": "my-work-project-bucket"
    }
}
```
`Environment`部分を追加していますが、`env.json`の定義に差し替えられる形になります。

```yaml:/work/template.yaml（抜粋）
Resources:
  StockRustFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment: 
        Variables:
            TEST: TEST
            AWS_S3_BUCKET: AWS_S3_BUCKET
```
デプロイ前動作確認にて問題ないことを確認

```bash:再ビルドが必要
$ cd /work
＄ sam build
$ sam local invoke --env-vars env.json {関数名} // 成功
```
次にデプロイ後の確認ですが、前述の通り`sam build`、`sam deploy`に`--env-vars`は存在しませんので、デプロイ自体できませんでした。

```bash
$ sam deploy --env-vars env.json
Usage: sam deploy [OPTIONS]
Try 'sam deploy -h' for help.

Error: no such option: --env-vars
```

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

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

デプロイできる他の方法を模索します。
私の手順では、`template.yaml`の`environment`に任意の文字列を定義しましたが、本来はここにデプロイ後に使う値が設定されており、デプロイ前確認時（`sam local invoke`）に別環境でテストする場合などに、上書きするような使い方が想定されているように思います。
ですので、用途が若干違うというか、今後`sam build`や`sam deploy`にも当該オプションがつくかというと難しいでしょう。

## 解決策③`--parameter-overrides`の利用
`--parameter-overrides`は、`sam local invoke`、`sam build`、`sam deploy`全てに存在するオプションですのでこちらを利用します。

![スクリーンショット 2021-09-29 13.14.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/ed5a960e-9c22-a980-6b29-762cdaf31dff.png)

```yaml:/work/template.yaml（抜粋）
Parameters:
   Test:
     Type: String
   AwsS3Bucket:
     Type: String

Resources:
  StockRustFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment: 
        Variables:
          TEST: !Ref Test
          AWS_S3_BUCKET: !Ref AwsS3Bucket
```
デプロイ前動作確認にて問題ないことを確認

```bash:再ビルドが必要
$ cd /work
$ sam build
$ sam local invoke {関数名} --parameter-overrides Test="HelloEnv" AwsS3Bucket="my-work-project-bucket" // 成功
```
デプロイ後動作確認にて問題ないことを確認

```bash:deploy時もオプションの指定が必要
$ sam deploy --parameter-overrides Test="HelloEnv" AwsS3Bucket="my-work-project-bucket"
```
実行時のパラメータ指定が多いとだいぶ厳しいですね。
④でこのパラメータをなくしていきます。

## 解決策④`samconfig.toml`にパラメータを定義する
```yaml:/work/template.yaml
//③と同じものを使うため、省略
```
こちらの公式の記載ルールに従い、実行時のパラメータを設定ファイルに記載していきます。

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

```toml:/work/samconfig.toml（抜粋）
version = 0.1
[default]
[default.global.parameters]
parameter_overrides = "Test=HelloEnv AwsS3Bucket=my-work-project-bucket"

[default.deploy]
[default.deploy.parameters]
#parameter_overrides = "Test=HelloEnv AwsS3Bucket=my-work-project-bucket"

[default.local_invoke]
[default.local_invoke.parameters]
#parameter_overrides = "Test=HelloEnv AwsS3Bucket=my-work-project-bucket"
```
`[default.deploy.parameters]`や`[default.local_invoke.parameters]`にそれぞれ別の定義をすることもできます。
今回は、環境ごとの違いがないので、`[default.global.parameters]`で共通設定しました。

デプロイ前動作確認にて問題ないことを確認。オプション指定も不要です。

```bash:再ビルドが必要
$ cd /work
$ sam build
$ sam local invoke {関数名} // 成功
```
デプロイ後動作確認にて問題ないことを確認。オプション指定も不要です。

```bash:deploy時もオプションの指定が不要となりました
$ sam deploy
```
![スクリーンショット 2021-09-29 13.51.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/63c329fd-6c33-a48b-31d8-d58f6f1861f8.png)

実行時のパラメータは略すことできましたが、`samconfig.toml`に情報が混在してしまいました。
また、現在は2つの変数だけですが数が増えたらみにくくなります。


## おわりに
現状は、④を使っていますが、一般的なのは③でしょうか。
結局同じ情報が`.env`と`samconfig.toml`で二つあるのですが。。

## 参考
https://zenn.dev/27ma4_ton10/articles/113f8b8bac07c7c9a49f

https://github.com/aws/aws-sam-cli/issues/1163

https://github.com/ozbillwang/sam-hello-world


試せませんでしたが、S3に`env.json`をS3にアップしてその場所を参照するようにする方法も見かけました。

https://dev.classmethod.jp/articles/tsnote-cloudformation-include-external-parameter-file/

```yaml:template.yaml抜粋
      Environment:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: s3://*******/environment.json
```