Flask Practices #3-1, cookiecutter-flask ベースプロジェクトの育て方 - 環境変数

[<<<前回](https://qiita.com/coleyon/items/b83ab42305d2b74cdff3) | [目次](https://qiita.com/coleyon/items/0c44b0cd88de82f27f2c) | 次回>>>

前回の続きです。

# 1. Config の育て方

前回の続きとして、flask-cookiecutter ベースの Flask アプリに、拡張・設定を追加する例を紹介します。

## 結果(コード)

育てた結果としてのコードを先に掲載します。``.env.example`` と ``settings.py`` を [このように変更しました](https://github.com/coleyon/flask-intro-panels/commit/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938)。以降は解説です。


## 考え方

WebApp つまり Flask とその Extensions には設定可能なパラメータが多数あり、主要な設定値が環境変数経由で指定できる作りになっています。しかし、cookiecutter-flask で生成したアプリは、ごく一部の設定項目しか用意されていません。それ自体は [YAGNI](https://ja.wikipedia.org/wiki/YAGNI) で良いですが、この記事は拡張・設定法についての解説なので、「今すぐ使わないけども容易に準備できる項目」を洗い出して、一通り設定可能にします。

以下のような考え方で、追加をしてみましょう。

* Flask とその Extensions の主要な Config を、環境変数経由で指定する
* ``.env.example`` を環境変数ファイルのひな型として、以下2種のコピーを使い分ける
  * production 環境向けの環境変数ファイル： ``.env``
  * development 環境向けの環境変数ファイル： ``.env.debug``

## ロードの流れは？

ローカルデバッグ実行時の、環境変数ローディングの関係と流れです。

> ``.env`` <--environs.read_env()-- ``settings.py`` <--Flask.config_from_object()-- ``app.py`` <--create_app()-- ``autoapp.py`` <--flask run-- ``IDE (VS Code)``

VSCode のデバッグ実行設定で、どの環境変数ファイルをロードするかを示すための環境変数 ``ENV_FILE_NAME`` を指定します。エントリポイントは ``autoapp.py`` です。

```json:.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [{
        "name": "Flask Debug",
        "type": "python",
        "request": "launch",
        "module": "flask",
        "env": {
            // entrypoint を autoapp.py とする
            "FLASK_APP": "autoapp.py",
            // VSCode デバッグ実行時に、環境変数 ENV_FILE_NAME へ .env.dev を指定する
            "ENV_FILE_NAME": ".env.dev",
        },
        "args": [
            "run",
            // "--no-debugger",
            "--no-reload"
        ],
        "jinja": true,
    }]
}
```

``autoapp.py`` は ``app.py`` を呼びます。

```python:autoapp.py
from panels.app import create_app
app = create_app()                                 # app.py を call
```

``app.py`` は ``settings.py`` を設定オブジェクトとしてロードします。

```python:panels/app.py
def create_app(config_object='panels.settings'):
    app = Flask(__name__.split('.')[0])
    app.config.from_object(config_object)          # panels.settings Object を設定としてロード
```

``settings.py`` は、 ``ENV_FILE_NAME`` 環境変数に指定された環境変数ファイルをロードします。[environs](https://pypi.org/project/environs/) パッケージが良い仕事をしています。

```python:panels/settings.py
from environs import Env
from os import environ as osenv

env = Env()
selectable_envfilename = osenv.get('ENV_FILE_NAME', default='.env')   # .env.dev ファイルをReadする
env.read_env(selectable_envfilename, recurse=False)

ENV = env.str('FLASK_ENV', default='production')    # FLASK_ENV 環境変数を ENV 変数にセット
```

環境変数ファイルに、設定値が定義されています。

```bash:.env.dev
# [mode]
#       TESTING=false
#       ENV=production
FLASK_ENV=development                               # development 環境向けの起動設定
# FLASK_ENV=production
```

例として、FLASK_ENVパラメタは、[Flask の ENV](https://flask.palletsprojects.com/en/1.1.x/config/#ENV) に対応します。

```text:Flaskドキュメント意訳
ENV 変数
　アプリがどの環境で実行しているかを区別する。
　FLASK_ENV 環境変数に、production, development いずれかを指定した結果が、ENV 環境変数にセットされる。
　デフォルト値は production である。
　development は開発向けであり、運用環境へのデプロイ時に指定してはならない。
```

つながっていますね。この考え方に沿って、他の設定値も記述してみた結果が [このコード](https://github.com/coleyon/flask-intro-panels/commit/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938) になります。

## Extensions の範囲とは？

インポートされている Extensions の一覧は、``extentions.py`` から確認できます。（そして、[Application Factory Pattern](https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/) を採用しているため、できるように維持していくと良い）

```python:panels/extensions.py
# -*- coding: utf-8 -*-
"""Extensions module. Each extension is initialized in the app factory located in app.py."""
from flask_bcrypt import Bcrypt
from flask_caching import Cache
# ...
```

``Pipfile``, ``Pipfile.lock`` に記載された Extentions も使用できる範囲と言えます。

```toml:Pipfile
[packages]
Flask = "==1.1.1"
Flask-Bcrypt = "==0.7.1"
Flask-Caching = ">=1.0.0"
# ...
```


## パラメタの範囲とは？

先ほど確認した「インポートされている Extensions の範囲」を踏まえて、指定可能なパラメタを洗い出します。対象となる Extensions のオフィシャルなドキュメントは次のような範囲になります。

* [Flask の設定値の一覧](https://flask.palletsprojects.com/en/1.1.x/config/) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L9)
  * Extension 達（importされているExtensions）の設定値の一覧
      * [flask_bcrypt](https://flask-bcrypt.readthedocs.io/en/latest/) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L174)
         * [bcrypt](https://github.com/pyca/bcrypt/)
      * [flask_caching](https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L105)
      * [flask_debugtoolbar](https://flask-debugtoolbar.readthedocs.io/en/latest/#configuration) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L93)
      * [flask_login](https://flask-login.readthedocs.io/en/latest/#configuring-your-application)
      * [flask_migrate](https://flask-migrate.readthedocs.io/en/latest/)
         * [alembic](https://alembic.sqlalchemy.org/en/latest/)
         * [sqlalchemy](https://docs.sqlalchemy.org/en/13/)
      * [flask_sqlalchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/#configuration-keys) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L140)
         * sqlalchemy
      * [flask_webpack](https://github.com/nickjj/flask-webpack) - [Code](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example#L167)
         * [webpack](https://webpack.js.org/concepts/configuration/)
         * node.js
      * [flask_wtf](https://flask-wtf.readthedocs.io/en/stable/config.html)
         * [wtforms](https://wtforms.readthedocs.io/en/stable/)

ドキュメントには、指定可能な環境変数の設定項目（Key）と設定値（Value）が書かれています。たとえば ``Flask`` 自体の [ENVパラメタは](https://flask.palletsprojects.com/en/1.1.x/config/#ENV) ・・・

> What environment the app is running in. Flask and extensions may enable behaviors based on the environment, such as enabling debug mode. The env attribute maps to this config key. This is set by the FLASK_ENV environment variable and may not behave as expected if set in code.
**Do not enable development when deploying in production.**
Default: ``production``

と書かれており、意訳すると次のような意味です。

> * ENV環境変数は、アプリがどの「環境」で動作するかを指定する
* FlaskとそのExtensionsは「環境」に応じて「振る舞い」を変える
* 「振る舞い」にはデバッグモード（development）と運用モード（production）がある
* "FLASK_ENV" 環境変数にセットされた値が、"ENV" キーにマップされる
* "ENV" 環境変数にプログラム・コード上から値を設定すると期待通りに動作しない可能性がある（つまり、FLASK_ENV 環境変数に対して "development" または "production" を指定する。"ENV" 環境変数に値をセットするような処理はコーディングしなくて良い。）
* develoment モードで運用環境にデプロイしてはならない
* デフォルト値は ``production`` である

よって、環境変数ファイルには次のように、2つのファイルに書き分ける考え方になります。[冒頭で示したコードにおけるこの行のこと](https://github.com/coleyon/flask-intro-panels/commit/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938#diff-cbfd64a28982a1818f2b5f16e7f9d634R47-R48)ですね。

```bash:.env.dev（development環境用）
FLASK_ENV=development
```

```bash:.env（production環境用）
FLASK_ENV=production      # または指定しない
```

両ファイルの取り扱いとしては、[.gitignore を用いて除外指定](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.gitignore#L56)することで git 管理対象外にしつつ、ひな型としての環境変数ファイルを [.env.example](https://github.com/coleyon/flask-intro-panels/blob/78273b781b98ad3e28b3a6a73d19ff7e8ebb3938/.env.example) として作成して管理対象に入れるようにします。

そして、このアプリをビルド・実行しようとする第三者は ``.env.example`` を様々な手段でコピー（``.envファイル`` や ``.env.devファイル`` や ``CI/CDツール上での実行時環境変数注入``）して使います。つまり、プログラムとパラメタ同士を分離（疎結合に）することができます。

## 分離するメリットの狙い

理想的には、環境変数の差し込み内容や差し込み方を変えるだけで（つまりプログラム・コードを変えずに）

* 誰でも（プロジェクトのオーナー、メンバー、ゲスト、CI/CDパイプラインなどの機械）
* どこでも（ローカルPCのDocker、AWS、GCP）
* 何でも（ローカルデバッグ、ユニットテスト、本番デプロイ）

いずれの使い分けもできるように、維持・拡張していく考え方となります。

ソースを変えないという事は、多重管理を排したり、試験環境でのテスト成績をもって運用環境での動作保証をしやすくしたり・・・と、語り切れない＆先達に語られてきた色々なメリットがあります。「語られてきた」内容の一部が、``12 Factor App`` にあたります。