Serverless Framework & Python 環境変数の使い方とステージの管理

この記事は、私の個人ブログ [Practical Cloud Python - SERVERLESS FRAMEWORK & PYTHON 環境変数の使い方](https://www.practicalcloudpython.com/2020/07/04/30/) を Qiita に移植したものです。[Practical Cloud Python](https://www.practicalcloudpython.com) の方もよろしくお願いします。

# Abstract

ステージ管理とコーディングで楽できる環境変数の管理方法を説明します。

# ソース

例題となるソースを公開していますので、実装例はこちらを見てください。

https://github.com/hassaku63/sls-env-example

Lambda (scheduled) -> SQS -> Lambda (Queue consumer) の構成になっています。

# 前提

以下のパッケージ、プラグインを利用しています。

Serverless Framework

- [Serverless Dotenv Plugin](https://www.serverless.com/plugins/serverless-dotenv-plugin)
- [Serverless Pseudo Parameters](https://www.serverless.com/plugins/serverless-pseudo-parameters)

Python

- [python-dotenv](https://pypi.org/project/python-dotenv/)

# 環境変数は .env.<stagename>と serverless.yml で宣言

例示のソースは Slack へのポストを想定したコードです。Tokenやチャンネル名が秘匿情報にあたりますので、 .env に記述しています。

Serverless Framework では、Serverless Dotenv Plugin を使うことで .env ファイルの定義をデプロイパッケージングの時に provider.environment に組み込んでくれます。

Dotenv plugin では、指定したステージの名前をサフィックスとする .env ファイルを自動でロードします。例えば、dev ステージであれば .env.dev を環境変数の定義として読み込みます( Automatic Env file name resolution のセクションを参照)。ステージごとにAPI Tokenなどを変更する設定はこの機能を利用します。

SQS や DynamoDB のリソース名の管理にも環境変数で宣言します。これは、コードの中から boto3 のクライアントを使う場合にキューやテーブルのリソース名が必要になるからです。

https://gist.github.com/hassaku63/0e4e61db60bd48b5d86459ceabb9dd34

```yaml
provider:
  name: aws
  runtime: python3.8
  profile: ${opt:profile, default}
  stage: ${opt:stage, dev}
  region: ${opt:region, us-east-1}
  timeout: 30
  environment:
    # To tell lambda function to AWS AccountId by use pseudo-parameters plugin.
    # AccountId is used to construct SQS Queue URL
    AWS_ACCOUNT_ID: '#{AWS::AccountId}'
    # MY_QUEUE_NAME is used to construct Queue URL in lambda.
    # And also convenient to reference resource ARN in IAM Policy statement
    MY_QUEUE_NAME: ${self:service}-my-queue-${self:provider.stage}
```

`${self:service}-my-queue-${self:provider.stage}` としている部分が今回のソースで利用しているキューの名前です。service や ステージ名を含めるのは、複数のデプロイステージを利用する場合にリソース名のユニーク性を確保するためです。

Python から SQS への Enqueue を行う場合は、ターゲットリソースの識別子として QueueUrl を指定する必要があります。QueueUrl を組み立てるためには AWS アカウント ID, AWS リージョン, QueueName の3つが必要です。Lambda の中でそれらを参照できるようにしたいので、環境変数で宣言しているというわけです（なお、リージョンだけは Lambda が実行時にデフォルトで環境変数として自動でセットしてくれるため自前で定義する必要がありません）。

環境変数を .env に書くべきか serverless.yml に書くべきか、という話もありますが、これは秘匿したい情報かどうか、という軸で判断すれば良いと思います。環境変数の数が多くなりすぎて冗長になってしまう、という場合は本当に環境変数じゃないと困るものだけをピックアップして、それだけを環境変数として宣言すると良いでしょう。（リソース名の一部分を構成する部分文字列など）本質的に重要でない部分などがあれば、それらは custom セクションで定義しておくのなどすると良いのではないかと思います。

# Python での環境変数の定義は settings.py に寄せる

Python から環境変数を扱う場合は os.environ を利用すると思います。しかし、この書き方では IDE の自動補完が効かないため、コーディング時に変数名の Typo に気づく方法がありません。デプロイして初めて環境変数のミスタイピングに気づく…というのはあまり開発体験がよろしくありません。また、Lambda handler の数が増えてきたとき、その都度 os.environ を使うのはなんだか DRY に違反しているような感じもしますね。

これを防ぐために、環境変数のロード、および Python 定数として定義してしまうための settings.py を作ります。すべての Python コードは、環境変数をこのモジュールから読み込むようにします。こうすることで、DRY を守りつつ、IDE の自動補完を活用できるようになり、ミスタイピングの可能性をへらすことができます。

https://gist.github.com/hassaku63/0e971fb0823aea561f33db880d0269e4

```python
"""
Declare all environment variables defined in .env.<stage-name> and serverless.yml as python variables.
If you would like to use environment variables in your code, you import this module then reference as constants of python variable.
By use this technique, you are able to use IDE auto-completion effectively when you writing python code.
It is useful to reduce miss-typing of environment variables.
"""
import os
import pathlib
import json
import dotenv

project_root_dir = pathlib.Path(__file__).parent / '../'

dotenv.load_dotenv()
MYAPP_STAGE_NAME = os.environ.get('MYAPP_STAGE_NAME')

stage_env_path = project_root_dir / f'.env.{MYAPP_STAGE_NAME}'
dotenv.load_dotenv(stage_env_path.resolve())

# Slack
SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN')
SLACK_CHANNEL = os.environ.get('SLACK_CHANNEL')

# AWS
AWS_ACCOUNT_ID = os.environ.get('AWS_ACCOUNT_ID')
# Notice: You don't need to define AWS_REGION in .env and serverless.yml
# because of lambda runtime automatically set AWS_REGION env var when execute.
AWS_REGION = os.environ.get('AWS_REGION') 

# SQS
MY_QUEUE_NAME = os.environ.get('MY_QUEUE_NAME', '')
```

このとき、ステージに応じた .env を読み込むために python-dotenv を利用しています。プロジェクトルートに .env が存在することを期待しています。Pathlib で settings の相対ディレクトリを探し、MYAPP_STAGE_NAME 環境変数を使ってステージ名を決定し、その名前を .env のサフィックスとして使用します。

※serverless-dotenv-plugin は NODE_ENV の定義をステージ名として使用することもできるので、独自のステージ管理用変数 (MYAPP_STAGE_NAME) ではなくこちらを使っても良いかもしれません。

# まとめ

Serverless Framework にも Python にも、.env を扱うための追加パッケージが使えます。これらを利用して環境変数をうまく使っていきましょう。

DynamoDB や SQS のリソース識別子など、コード中でリソース名への参照がいる場合があります。このような場合にも環境変数が使えます。環境変数として定義しておくことで、テンプレート中のリソース名参照においても変数を再利用することができます。

os.environ を使って環境変数をPython の定数にまとめるためのモジュールを作りましょう。IDE の自動補完も使えるし、記述が一箇所に集約されるのでミスタイピングが起こりづらくなり、楽です。