Pythonで環境別の開発・運用を考える

# はじめに

Python のシステムを開発・運用していく上で、開発・検証・本番と**環境別に違う情報を管理する**ことになると思います。

以前私は、Amazon ECS に Python のシステムを構築する際に、環境別の情報を**環境変数**に設定していました。しかし、開発や運用をしているとやりにくい点が出てきました。

かといって、要件に合いそうなライブラリもなさそうだったので、下記の記事を参考にして環境別設定ファイルの構成を考えてみました。

https://qiita.com/t2kojima/items/64c7e064ea6609915705

※ 一個人の見解ですので必ずしも正解や正攻法ではありません。より良い方法があればお気軽にフィードバック頂ければと思います。


# 課題

Amazon ECS に Python のシステムを構築する際は、OS の**環境変数**を用いて環境別の設定値を取得していました。そして、環境変数の設定は AWS CloudFormation の [AWS::ECS::TaskDefinition - ContainerDefinition - Environment](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-containerdefinitions.html#cfn-ecs-taskdefinition-containerdefinition-environment) で行っていました。

この際に開発や運用がやりにくいなと思っていたのは以下の点です。

- Python の標準モジュール `os.getenv` で環境変数を取得すること
    - 一律、文字列になってしまので型変換が必要になってしまう
- ローカル開発環境用の環境変数（起動パラメータ）と AWS CloudFormation 用の環境変数が分散し、二重管理になってしまう
    - AWS CloudFormation の反映忘れなどが起こってしまっていました


# 構成・前提

| 項目   | バージョン |
| ------ | ---------- |
| Python | 3.10.6     |
| Visual Studio Code | 1.70.0     |

なおローカル開発環境の OS は、 Windows10 を想定しています。


# 実装

## 環境

本記事では下記の環境を想定します。

| 環境名      | 内容     |
| ----------- | -------- |
| development | 開発環境 |
| staging     | 検証環境 |
| production  | 本番環境 |

## ファイル構成

- `environments` ディレクトリを作成
- `environments` 直下に、`__init__.py` ファイル、および環境別のディレクトリを作成
- 環境別ディレクトリ直下に、`env.py` を作成し、環境別変数を記述

```
    PythonEnv               # ルートディレクトリ
    ├─ .vscode
    │   └─ launch.json      # VS Code 起動設定
    │   
    └─ src                  # ソースディレクトリ
        ├─ environments     # 環境ファイル用ディレクトリ
        │   ├─ development  # 開発環境
        │   │   └─ env.py   # 環境別設定ファイル
        │   │   
        │   ├─ production   # 本番環境
        │   │   └─ env.py   # 環境別設定ファイル
        │   │   
        │   ├─ staging      # 検証環境
        │   │   └─ env.py   # 環境別設定ファイル
        │   │   
        │   └─ __init__.py  # パッケージの初期化処理ファイル
        │   
        └─ main.py          # 呼び出し元サンプル
        
```


### `__init__.py`

```python:__init__.py
import os

try:
    # 例外を投げずに os.getenv で取得して、None判定したり、デフォルト値を設定してもOK
    env = os.environ['ENV']
except KeyError:
    print('環境変数:ENV が設定されていません。')
    raise Exception

if env == 'production':
    from environments.production.env import *
elif env == 'staging':
    from environments.staging.env import *
elif env == 'development':
    from environments.development.env import *
else:
    print(f'環境変数:ENV の値が不正です（{env}）。')
    raise Exception

```

:::note info
**ポイント**
- `env = os.environ['ENV']` にて、環境変数「`ENV`」を取得します。
  この環境変数だけは実行環境にセットしてください！
- 環境変数の設定忘れを検知したいので、`raise` するようにしています
- 環境変数の値に応じて、各環境別設定ファイルを `import` します
:::


### environments/development/env.py

例として、開発環境の環境別設定ファイルを記載します。

```python:env.py
ENV = 'development'
ENV_API_URL = f'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/{ENV}'
ENV_NUM = 10
ENV_FLG = True
```

:::note info
**ポイント**
- 1行目は親ディレクトリと同じ文字列をセットしてください
- f文字列を使えば変数埋め込みが可能です
- 型はそのまま反映されます（タプル、配列、辞書ももちろん可能）
- ここではルールとして接頭辞「`ENV_`」を付けていますが、任意で問題ありません
:::
1行目がイケてないですが、 `__init__.py` から渡す方法もなく、もう1度 `os` モジュールから呼び出すのもなぁ……と思ったので、ベタで書きます（環境名が変わることは早々ないだろう……という妥協）。


## 実装サンプル（main.py）

呼び出し元の記述サンプルを記載します。

```python:main.py
from environments import *

print('main.py')
print(ENV)
print(ENV_API_URL)
print(ENV_NUM)
print(ENV_FLG)
```

:::note info
**ポイント**
- `from environments import *` で環境別設定ファイルを読み込みます（個別のインポートも可能です）
- 定義した変数名をそのまま使用可能です
:::

:::note warn
**注意**
- 変数名が重複しないように気をつけてください
- 呼び出したモジュール内で値の上書きはできてしまうので気をつけてください（`os` モジュールを使った場合も同様なので特段対策せず）
:::

### 実行結果

```
main.py
development
https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/development
10
True
```

## 運用例

環境を特定するための環境変数「`ENV`」だけは定義する必要があります。

Amazon ECS の記述をする AWS CloudFormation であれば、コンテナ定義の環境変数に。
ローカル開発環境であれば、端末の環境変数や実行時の環境変数設定に。
AWS Lambda であれば、Lambda環境変数に。

## 補足

### ローカル開発環境での実行方法

VSCode の場合は、`launch.json` 等で、環境変数を設定してください。

```json:launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: 現在のファイル",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": true,
            "env": {
                "ENV": "development",
            }
        }
    ]
}
```

### 環境別設定ファイルを別のGitリポジトリで管理したい場合

見出しのような要件がある場合は、下記の記事と同様の構成にすることで対応可能です。

https://qiita.com/vidro/items/b86a6697b718fa6cb9a8#%E9%81%8B%E7%94%A8%E4%BE%8B

### python-dotenv

Python には環境変数周りをサポートするライブラリ「[python-dotenv](https://github.com/theskumar/python-dotenv)」があります。
ただ、今回の要件には合わなかったので使用していません
（`os.getenv` を経由することになるので、一律文字列になってしまう）。

### 環境別設定ファイルの外出し

Amazon ECS では環境変数をファイルに外出しできるオプションもあります。
しかし、当記事では `os.getenv` を用いない方法で解決したかったため、下記の機能は使用していません。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/taskdef-envfiles.html

# おわりに

その他にも秘匿情報は AWS Secrets Manager を用いる等の方法もあります。
異なる情報を適切に管理する上で、当記事の内容が一助になれば幸いです。