LambdaのコードとCFnテンプレートの環境変数を同時に変更すると一瞬死ぬかも

# はじめに
Lambda関数をデプロイする際、CloudFormation（以下、CFn）を利用される方も多いと思います。
今まで知らなかったのですが、Lambda関数に乗せるソースコードとCFnテンプレートの両方で環境変数名を同時に変更するとダウンタイムが発生する場合があったので、備忘録として検証記事を書きます。

# 使用するCFnテンプレート
今回の検証に利用する、最小構成のLambda関数をデプロイするためのCFnテンプレートです。
Lambda関数のリソース定義において、Environmentに`HOGE: 1234`を追加し、Codeに`HOGE`を参照するPythonコードを直接書いています。

``` minimum_template.yml
AWSTemplateFormatVersion: 2010-09-09

Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.9
      FunctionName: hoge
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Environment:
        Variables:
          HOGE: 1234
      Code:
        ZipFile: |
          import os
          def lambda_handler(event, context):
              print(os.environ['HOGE'])

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/lambda/hoge

```

# ダウンタイムの発生を検証してみる
上記のCFnテンプレートを利用し、ソースコードとCFnテンプレートの環境変数名を同時に変更した場合に実際にダウンタイムが生じることを見てみます。
以下、CFnテンプレートのファイル名は`minimum_template.yml`とします。

## 検証手順

### 1. CFnテンプレートをデプロイする
AWS CLIを利用し、cloudformationの`create-stack`コマンドを実行してスタックを新規作成します。実行後、hogeという名前のスタックとLambda関数が作成されます。

``` create_stack.sh
aws cloudformation create-stack --stack-name hoge --template-body file://minimum_template.yml --capabilities CAPABILITY_NAMED_IAM
```

### 2. 環境変数名を変更して再デプロイする
手順1でのデプロイ完了後、以下のようにLambda関数リソース定義のEnvironmentとCodeの2箇所に含まれている`HOGE`を`FUGA`に変更します。
``` minimum_template.yml
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.9
      FunctionName: hoge
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Environment:
        Variables:
          FUGA: 1234  # HOGE -> FUGAに変更
      Code:
        ZipFile: |
          import os
          def lambda_handler(event, context):
              print(os.environ['FUGA'])  # HOGE -> FUGAに変更
```
上記変更後、スタックを更新するために`update-stack`コマンドを実行します。
``` update_stack.sh
aws cloudformation update-stack --stack-name hoge --template-body file://minimum_template.yml --capabilities CAPABILITY_NAMED_IAM
```

### 3. スタック更新中にLambda関数を実行し続ける
スタック更新中にダウンタイムが生じることを確認したいため、AWS CLIを利用してlambdaの`invoke`コマンドを実行し続けます。
`--log-type`オプションに`Tail`を指定するとレスポンスとしてLogResult（Lambda関数の実行ログがbase64エンコードされた文字列）を含むjsonが返却されるため、これをjqコマンドでパースしたあとbase64コマンドでデコードしています。
``` invoke_lambda.sh
while true; do
  date
  echo `aws lambda invoke --function-name hoge --log-type Tail output.txt` | jq -r .LogResult | base64 -d; echo
done

```

## 検証結果
以下に手順3の実行結果を載せます。今回の場合、`HOGE`の参照失敗によるKeyErrorが約7秒間発生し続けていることがわかります。Lambda関数のソースコード更新よりも先に環境変数の更新が行われて`HOGE`が削除されたことにより、一時的にコード側で`HOGE`が参照できなくなっているように見えます。
```
2022年 7月26日 火曜日 17時23分45秒 JST
START RequestId: 0f33223b-fb0b-4e4c-85e7-6116d42b63bd Version: $LATEST
1234
END RequestId: 0f33223b-fb0b-4e4c-85e7-6116d42b63bd
REPORT RequestId: 0f33223b-fb0b-4e4c-85e7-6116d42b63bd	Duration: 0.83 ms	Billed Duration: 1 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分45秒 JST
START RequestId: 06605b53-099b-49b8-bee6-4a3ebc2b691c Version: $LATEST
1234
END RequestId: 06605b53-099b-49b8-bee6-4a3ebc2b691c
REPORT RequestId: 06605b53-099b-49b8-bee6-4a3ebc2b691c	Duration: 0.86 ms	Billed Duration: 1 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分46秒 JST
START RequestId: a6b389a0-97ab-4702-8875-d604855ef8ce Version: $LATEST
1234
END RequestId: a6b389a0-97ab-4702-8875-d604855ef8ce
REPORT RequestId: a6b389a0-97ab-4702-8875-d604855ef8ce	Duration: 0.91 ms	Billed Duration: 1 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分47秒 JST
START RequestId: 217f85b5-ed2d-4839-acd4-6d452367893d Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 217f85b5-ed2d-4839-acd4-6d452367893d
REPORT RequestId: 217f85b5-ed2d-4839-acd4-6d452367893d	Duration: 12.99 ms	Billed Duration: 13 ms	Memory Size: 128 MB	Max Memory Used: 36 MB	Init Duration: 106.15 ms	

2022年 7月26日 火曜日 17時23分48秒 JST
START RequestId: 9362f269-cab2-4a2e-81b0-b239264663dd Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 9362f269-cab2-4a2e-81b0-b239264663dd
REPORT RequestId: 9362f269-cab2-4a2e-81b0-b239264663dd	Duration: 1.91 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分49秒 JST
START RequestId: 87ee1131-fdaa-4c57-8d10-0c5a1486d621 Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 87ee1131-fdaa-4c57-8d10-0c5a1486d621
REPORT RequestId: 87ee1131-fdaa-4c57-8d10-0c5a1486d621	Duration: 1.77 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分50秒 JST
START RequestId: 16073262-fffb-4f16-991b-8f02bca1d08d Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 16073262-fffb-4f16-991b-8f02bca1d08d
REPORT RequestId: 16073262-fffb-4f16-991b-8f02bca1d08d	Duration: 1.11 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分51秒 JST
START RequestId: 483d5950-35b7-43b9-bae9-cf5d1bbb7482 Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 483d5950-35b7-43b9-bae9-cf5d1bbb7482
REPORT RequestId: 483d5950-35b7-43b9-bae9-cf5d1bbb7482	Duration: 1.19 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分52秒 JST
START RequestId: fa098f17-6c93-4688-b982-f0ae7882ff48 Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: fa098f17-6c93-4688-b982-f0ae7882ff48
REPORT RequestId: fa098f17-6c93-4688-b982-f0ae7882ff48	Duration: 1.32 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分53秒 JST
START RequestId: 1bb5f8a5-61b3-48f3-a128-bf3aab39fee9 Version: $LATEST
[ERROR] KeyError: 'HOGE'
Traceback (most recent call last):
  File "/var/task/index.py", line 3, in lambda_handler
    print(os.environ['HOGE'])
  File "/var/lang/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from NoneEND RequestId: 1bb5f8a5-61b3-48f3-a128-bf3aab39fee9
REPORT RequestId: 1bb5f8a5-61b3-48f3-a128-bf3aab39fee9	Duration: 1.26 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	

2022年 7月26日 火曜日 17時23分54秒 JST
START RequestId: 05bc1601-0bf0-4042-a12c-1cd63b9dcf74 Version: $LATEST
1234
END RequestId: 05bc1601-0bf0-4042-a12c-1cd63b9dcf74
REPORT RequestId: 05bc1601-0bf0-4042-a12c-1cd63b9dcf74	Duration: 1.23 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 36 MB	Init Duration: 120.57 ms	

2022年 7月26日 火曜日 17時23分55秒 JST
START RequestId: 8cd572bf-4e19-432a-80f1-31d10b0f0471 Version: $LATEST
1234
END RequestId: 8cd572bf-4e19-432a-80f1-31d10b0f0471
REPORT RequestId: 8cd572bf-4e19-432a-80f1-31d10b0f0471	Duration: 1.61 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 36 MB	

2022年 7月26日 火曜日 17時23分56秒 JST
START RequestId: 356670a1-c72c-44cf-8eb2-eac4b4450973 Version: $LATEST
1234
END RequestId: 356670a1-c72c-44cf-8eb2-eac4b4450973
REPORT RequestId: 356670a1-c72c-44cf-8eb2-eac4b4450973	Duration: 0.95 ms	Billed Duration: 1 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	
```

# おわりに
Lambda関数に乗せるソースコードとCFnテンプレートの両方で環境変数名を同時に変更するとダウンタイムが発生することを検証しました。

ソースコードよりも先に環境変数がデプロイされているようなので、今回のように環境変数名の変更を行いたい場合、CFnテンプレート側の対策として変更前の環境変数を残しておくか、ソースコード側の対策として環境変数が取得できない場合にデフォルト値を取得できるようにしておく、などがあると思いますが、なるべくやりたくないですね……。


## 最後に宣伝

株式会社オプティマインドでは、一緒に働く仲間を大募集中です。
カジュアル面談も大歓迎ですので、気軽にお声がけください。

【エンジニア領域の募集職種】
●[ソフトウェアエンジニア](https://hrmos.co/pages/optimind/jobs/0002012)
●[QAエンジニア](https://hrmos.co/pages/optimind/jobs/0002011)
●[Androidアプリエンジニア](https://hrmos.co/pages/optimind/jobs/0002008)
●[組合せ最適化アルゴリズムエンジニア](https://hrmos.co/pages/optimind/jobs/0002013)
●[経路探索アルゴリズムエンジニア](https://hrmos.co/pages/optimind/jobs/0002014)
●[バックエンドエンジニア](https://hrmos.co/pages/optimind/jobs/0002015)
●[インフラエンジニア](https://hrmos.co/pages/optimind/jobs/0002016)
●[UXUIデザイナー](https://hrmos.co/pages/optimind/jobs/0002010)

【ビジネス領域の募集職種】
●[セールスコンサルタント](https://hrmos.co/pages/optimind/jobs/0002001)
●[オペレーションコンサルタント](https://hrmos.co/pages/optimind/jobs/0002027)



『オプティマインドってどんな会社？』については、[こちら](https://www.optimind.tech/recruit/)から
Wantedlyでも[こちらで募集中](https://www.wantedly.com/companies/optimind/projects)