Flaskプロジェクトのセットアップ(TDD/Docker/Postgresなど) - Part 1
著者:Leonardo Giordani - 05/07/2020 Updated on Aug 22, 2021
はじめに
インターネット上にはウェブフレームワークの使い方やウェブアプリケーションの作り方を教えてくれるチュートリアルがたくさんあり、その多くがFlaskを取り上げています。
では、なぜ別のチュートリアルを?最近、私は小さな個人的なプロジェクトに取り組み始め、フレームワークの知識をリフレッシュする良い機会だと思いました。そのため、私がよく勧めるクリーンなアーキテクチャを一時的にやめて、いくつかのチュートリアルに従ってゼロから始めました。私の開発環境はあっという間に散らかってしまい、しばらくすると、グローバルな設定にはとても満足できないことに気づきました。
そこで、もう一度ゼロから始めることにしました。今回は、開発環境に求める要件を書き出してみました。また、アプリケーションを本番環境にデプロイすることがいかに複雑であるかをよく知っているので、できるだけ「デプロイしやすい」セットアップにしたいと考えています。多くのプロジェクトがレガシーなセットアップに悩まされているのを見てきましたし、最小限の計画でそのような問題を回避できることもよく知っていたので、これは他の開発者にとっても興味深いのではないかと思いました。私はこの設定が他のものより優れているとは考えていませんが、単に異なる問題に対処しているだけです。
この記事で学べること
この記事では、私が取り組んでいる実際のFlaskプロジェクトをどのようにセットアップしたかを、ステップバイステップで説明します。これは多くの可能なセットアップのうちの1つに過ぎず、私の選択は個人的な好みの問題であり、このセクションで述べるいくつかの目標によって決定されていることを理解することが重要です。要件を変更すると、明らかに構造を変更することになります。この記事の目的は、プロジェクトのセットアップでは、様々なことを前もって考慮することができ、適切に取り組むには遅すぎるかもしれない不確定な将来に残すことはないことを示すことです。
私のセットアップの要件は以下の通りです。
本番、開発、テストで同じデータベースエンジンを使用する。
一時的ななデータベースでテストを行う
本番環境では、静的な設定以外は変更せずに実行する。
データベースを初期化し、マイグレーションを管理するコマンドがあること。
空のデータベースから「シナリオ」を起動し、クエリをテストできるサンドボックスを作成する方法がある。
ローカル環境で本番をシミュレートできる
技術的には、WebフレームワークとしてFlaskを使用します。また、HTTPサーバーとしてGunicornを(本番環境で)使用し、データベース部分にはPostgresを使用する予定です。本番環境の構築方法はここでは紹介しませんが、私は日々AWSを使って仕事をしているので、特定のソリューションに固執しすぎないようにしながら、AWSの要件をいくつか考慮していきたいと思います。
一般的なアドバイス
適切なセットアップは将来への投資です。TDD(Test-Driven Development)では、後でバグを見つけて修正するために10倍の時間を費やすことを避けるために、今(テストを書く)時間を費やすしますが、プロジェクトのセットアップには時間が必要で、「物事が起こるのを見たい」という欲求を挫くかもしれません。適切なセットアップは、忍耐とコミットメントを必要とする規律です
もし準備ができているなら、私と一緒にFlaskアプリケーションの素晴らしいセットアップに向けた旅に出ましょう。
黄金律
適切なインフラ作業の黄金律は、「情報源は1つでなければならない」ということです。プロジェクトの設定は、異なるファイルやリポジトリに散らばっていてはいけません(安全に保管されなければならない機密情報はこの限りではありません)。設定はアクセス可能でなければならず、異なるツールのニーズに対応するために異なるフォーマットに変換することも容易でなければなりません。このため、設定はJSON、YAML、INIなどの静的なファイル形式で保存し、さまざまなプログラミング言語やツールで読み込んで処理できるようにする必要があります。
このチュートリアルでは、PythonとTerraformの両方で読み取ることができ、ECS on AWSでネイティブに使用されているJSON形式を選択しました。
ステップ1 - 要件と編集方針
私のPython要件の標準的な構造は、production.txt、development.txt、testing.txt の3つのファイルを使用します。これらはすべてrequirementsという同じディレクトリに格納され、階層的に接続されています。
code: requirements/production.txt
## このファイルはいまは空
code: requirements/testing.txt
-r production.txt
code: requirements/development.txt
-r testing.txt
また、最終ファイルのrequirements.txtは本番用のものを指しています。
code: requirements.txt
-r requirements/production.txt
これにより、必要のないパッケージをインストールしないように要件を分けることができ、本番環境でのデプロイを大幅に高速化し、必要なものを可能な限り残しておくことができました。本番環境にはプロジェクトの実行に必要な最低限の要件が含まれ、テスト環境にはコードをテストするためのパッケージが追加され、開発環境には開発に必要なツールが追加されています。この設定のちょっとした欠点は、例えばHTTPサーバーのように、本番環境で必要なものがすべて開発環境で必要になるとは限らないことです。しかし、これが私のローカルセットアップに大きな影響を与えるとは思いませんし、もし本番環境と開発環境のどちらかを選ばなければならないのであれば、私は本番環境をスリムで整然とした状態に保ちたいと考えています。
lintersはすでにシステム全体にインストールされていますが、コードのフォーマットにblackを使っているので、私がやっていることを受け入れるようにflake8を設定しなければなりません。
code: .flake8
# Recommend matching the black line length (default 88),
# rather than using the flake8 default of 79:
max-line-length = 100
ignore = E231
これは明らかに非常に個人的な選択であり、あなたは異なる要件を持っているかもしれません。時間をかけてエディタとリンターを適切に設定してください。プログラマーにとってのエディタは、バイオリニストにとってのバイオリンのようなものだということを忘れないでください。それを知り、それを大切にしなければなりません。それゆえ、きちんと設定してください。
また、この時点で私は仮想環境を作成し、それを起動します。
Gitコミット
このステップで行われた変更は、このリポジトリリの Gitコミットで確認できますし、ファイルを参照 することもできます。 参考資料
flake8: Python コードスタイルガイドに従ったフォーマッタ black: 妥協を許さない Python コードフォーマッタ ステップ2 - Flaskプロジェクトのボイラプレート
これはFlaskアプリケーションになるので、最初にやるべきことはFlask自体をインストールすることです。これはすべての段階で必要とされるため、production.txt に入れます。
code: requirements/production.txt
Flask
ここで、開発に必要なものをインストールします。
code: bash
$ pip install -r requirements/development.txt
前に見たように、このファイルはテスト用と本番用の要件も自動的にインストールします。
次に、Flaskフレームワークに直接関連するすべてのコードを保管するディレクトリが必要です。また、ここでアプリケーションの設定を作成し始めます。applicationディレクトリを作成し、その中にconfig.py ファイルを作成します。
code: application/config.py
class Config(object):
"""Base configuration"""
class ProductionConfig(Config):
"""Production configuration"""
class DevelopmentConfig(Config):
"""Development configuration"""
class TestingConfig(Config):
"""Testing configuration"""
TESTING = True
Flaskアプリケーションを構成する方法はたくさんありますが、その1つにPythonオブジェクトを使う方法があります。これにより、継承を活用して重複を避けることができるので(これは常に良いことです)、私はこの方法を選択しています。
設定に関わる変数やパラメータを理解することが重要です。ドキュメントに明記されているように、FLASK_ENV と FLASK_DEBUG はアプリケーションの外で初期化する必要があります。これは、エンジンの起動後にこれらの変数を変更するとコードが誤動作する可能性があるからです。さらに、FLASK_ENV 変数は development と production の 2 つの値しか持つことができず、主な違いはパフォーマンスにあります。最も重要なことは、FLASK_ENV が development の場合、FLASK_DEBUG は自動的に True になるということです。要約すると次のようなガイドラインになります。
アプリケーションの設定でDEBUGやENVを設定するのは無意味で、これらは環境変数にするべきものです。
通常、FLASK_DEBUG を設定する必要はなく、FLASK_ENV を開発用に設定するだけで構いません。
テストではデバッグ サーバーをオンにする必要はないので、その段階で FLASK_ENV を production に設定することができます。ただし、TESTING を True に設定する必要があるので、これはアプリケーション内で行う必要があります。
ここでは、アプリケーションの作成と適切な設定を行う必要があります。
code: application/app.py
from flask import Flask
def create_app(config_name): # Point 1
app = Flask(__name__)
config_module = f"application.config.{config_name.capitalize()}Config" # Point 2
app.config.from_object(config_module)
@app.route("/") # Point 3
def hello_world():
return "Hello, World!"
return app
私は、アプリケーションファクトリ(Point 1)を使用することにしました。このファクトリは、文字列 config_name を受け取り、それを config オブジェクト(Point 2)の名前に変換します。例えば、config_nameがdevelopmentの場合、変数config_moduleはapplication.config.DevelopmentConfigとなり、app.config.from_objectがそれをインポートできるようになります。また、サーバーが動作しているかどうかを簡単に確認できるように、標準的な「Hello, world!」のroute(Point 3)を追加しました。
最後に、アプリケーションファクトリを実行し、パラメータconfig_nameに正しい値を渡して、アプリケーションを初期化するものが必要です。WSGIは標準的な仕様なので、これを使用することで、本番環境で使用するHTTPサーバー(例えばGunicornやuWSGIなど)がすぐに動作することを確信しています。
code: wsgi.py
import os
from application.app import create_app
ここでは、変数FLASK_CONFIGからconfig_nameの値を読み取ることにしました。 これはフレームワークから要求された変数ではありませんが、Flaskアプリケーションの構造と密接に関連しているため、とにかく接頭辞FLASK_を使用することにしました。
この時点で、Flask開発サーバーを実行することができます。
code: bash
$ FLASK_CONFIG="development" flask run
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
Environment: productionと表示されているのは、FLASK_ENVがまだ設定されていないためですのでご注意ください。ブラウザで http://127.0.0.1:5000/ にアクセスすると、グリーティングメッセージを見ることができます。
Git コミット
このステップで行った変更は、この Git コミット で確認できるほか、ファイルを参照 することもできます。 参考資料
WSGI: Python Webサーバーゲートウェイインターフェース 次の記事にWSGIのセクションがあります。
ステップ3 - アプリケーションの設定
冒頭で述べたように、私は静的なJSON設定ファイルを使用するつもりです。JSONを選択したのは、Terraformを含む多くのプログラミング言語からアクセス可能な、広く普及しているファイル形式であることが理由です。
code: config/development.json
[
{
"name": "FLASK_ENV",
"value": "development"
},
{
"name": "FLASK_CONFIG",
"value": "development"
}
]
JSONファイルから変数を抽出して環境変数に変換するスクリプトが必要なのは明らかなので、自分で manage.py ファイルを書き始めることにします。これはPythonのWebフレームワークの世界ではかなり標準的な概念で、Djangoが始めた伝統です。アイデアとしては、開発サーバーの起動/停止やデータベースの移行管理など、すべての管理機能を一元化することです。flaskのように、これは部分的にはflask自身のコマンドによって行われますが、当面は適切な環境変数を提供してそれをラップする必要があります。
code: manage.py
import os
import json
import signal
import subprocess
import click
# Ensure an environment variable exists and has a value
def setenv(variable, default):
os.environvariable = os.getenv(variable, default) setenv("APPLICATION_CONFIG", "development") # Point 1
# Read configuration from the relative JSON file
config_json_filename = os.getenv("APPLICATION_CONFIG") + ".json" # Point 2
with open(os.path.join("config", config_json_filename)) as f:
config = json.load(f)
# Convert the config into a usable Python dictionary
for key, value in config.items():
setenv(key, value)
@click.group()
def cli():
pass
@cli.command(context_settings={"ignore_unknown_options": True})
@click.argument("subcommand", nargs=-1, type=click.Path())
def flask(subcommand): # Point 3
cmdline = "flask" + list(subcommand) try:
p = subprocess.Popen(cmdline)
p.wait()
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
p.wait()
cli.add_command(flask)
if __name__ == "__main__":
cli()
このファイルに実行権をつけることを忘れないでください。
code: bash
$ chmod 775 manage.py
ご覧のように、私はflaskのコマンドを実装するのに推奨される方法であるclickを使用しています。flaskメインスクリプトのサブコマンドをカスタマイズするのに使うかもしれないので、1つのツールにこだわり、manage.py のスクリプトにも使うことにしました。
APPLICATION_CONFIG(Point 1)という変数は、私が指定する必要がある唯一のもので、そのデフォルト値はdevelopmentです。この変数から、フルコンフィグレーション(Pont 2)を含むJSONファイルの名前を推測し、そこから環境変数をロードしています。関数flask (Point 3)は、Flaskが提供するコマンドflaskを単純にラップしたもので、./manage.py flask <subcommand> を実行してdevelopmentの設定を使って実行したり、env APPLICATION_CONFIG="foobar" ./manage.py flask <subcommand> を実行してfoobarの設定を使ったりすることができます。
環境変数をお互いに混同しないように、明確にしておきます。
APPLICATION_CONFIGは、私のプロジェクトに厳密に関連しており、変数自体に指定された名前のJSON設定ファイルをロードするためにのみ使用されます。
FLASK_CONFIGは、Flaskアプリケーションの設定を含むPythonオブジェクトを選択するために使用されます(application/app.py と application/config.pyを参照)。変数の値はクラスの名前に変換されます。
FLASK_ENV は、Flask 自体が使用する変数で、その値は Flask によって指示されます。前節のリソースで紹介した設定ドキュメントを参照してください。
これで、開発サーバーを実行することができます
code: bash
$ ./manage.py flask run
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 172-719-201
FLASK_ENVがdevelopmentに設定されているため、Environment: developmentと表示されていることに注意してください。先ほどと同じように、http://127.0.0.1:5000/ にアクセスしてみると、すべてが稼動していることがわかります。
Git コミット
この手順で行った変更内容は、Git コミット で確認するか、ファイルを参照することができます。 参考資料
Click: コマンドラインインターフェースを作成するための Python パッケージ ステップ4 - コンテナとオーケストレーション
Dockerを使う以上に開発をシンプルにする方法はありません。
また、Dockerを使う以上に人生を複雑にする方法もありません。
ご推察の通り、私はDockerに対して複雑な感情を抱いています。誤解しないでほしいのですが、Linuxコンテナは素晴らしいコンセプトですし、Dockerはとても便利です。また、複雑な技術でもあり、適切に設定するためには、時に多くの作業が必要になります。今回のケースでは設定は非常に簡単ですが、データベースサーバーを使用する際に大きな複雑さがありますので、後で説明します。
アプリケーションをDockerコンテナで実行することで、アプリケーションを分離し、本番環境で実行する方法をシミュレートすることができます。ここではdocker-composeを使用していますが、これは開発環境(少なくともデータベース)で他のコンテナが稼働していることを想定しているためで、docker-composeの設定ファイルが環境変数を補間できることを利用しています。もう一度、環境変数 APPLICATION_CONFIG を通じて、正しい JSON ファイルを選択し、その値を環境変数にロードしてから、docker-compose ファイルを実行します。
まず最初に、Flaskアプリケーション用のイメージが必要です。
code: docker/Dockerfile
FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /opt/code
RUN mkdir /opt/requirements
WORKDIR /opt/code
ADD requirements /opt/requirements
RUN pip install -r /opt/requirements/development.txt
ご覧のように、requirementsディレクトリがイメージにコピーされ、Dockerが作成時にpip installコマンドを実行できるようになっています。実行時には、コードディレクトリ全体がイメージにライブマウントされます。
これは明らかに、requirements/development.txt を変更するたびにイメージを再構築する必要があることを意味します。これは複雑なプロセスではないので、今のところ手動で行うことにしています。イメージを実行するには、docker-compose用の設定ファイルを作成します。
code: docker/development.yml
version: '3.4'
services:
web:
build:
context: ${PWD}
dockerfile: docker/Dockerfile
environment:
FLASK_ENV: ${FLASK_ENV}
FLASK_CONFIG: ${FLASK_CONFIG}
command: flask run --host 0.0.0.0
volumes:
- ${PWD}:/opt/code
ports:
- "5000:5000"
ご覧のように、docker-composeの設定ファイルは、環境変数をネイティブに読み取ることができます。これを実行するには、まずdocker-compose自体を requirements/development.txt に追加する必要があります。
code: requirements/development.txt
-r testing.txt
docker-compose
pip install -r requirements/development.txt でインストールし、次の手順でイメージをビルドします。
code: bash
$ FLASK_ENV="development" FLASK_CONFIG="development" docker-compose -f docker/development.yml build web
これは、Dockerが必要なレイヤーをすべてダウンロードし、reequirementsのパッケージをインストールする必要があるため、時間がかかります。
ここでは環境変数を明示的に渡していますが、これはdocker-composeをまだManageスクリプトに組み込んでいないためです。イメージがビルドされたら、↑のコマンドで実行できます。
code: bash
$ FLASK_ENV="development" FLASK_CONFIG="development" docker-compose -f docker/development.yml up
このコマンドを実行すると、次のような出力が得られます。
code: CONSOLE
Creating network "docker_default" with the default driver
Creating docker_web_1 ... done
Attaching to docker_web_1
web_1 | * Environment: development
web_1 | * Debug mode: on
web_1 | * Restarting with stat
web_1 | * Debugger is active!
web_1 | * Debugger PIN: 234-361-737
Ctrl-Cを押すとコンテナを停止し、システムを潔く切り離すことができます。up -d docker-compose というコマンドを実行すると、docker-compose がデーモンとして実行され、現在のターミナルの操作ができるようになります。docker-composeが起動している場合、docker psを実行すると、次のような出力が表示されます。
code: CONSOLE
CONTAINER ID IMAGE COMMAND ... PORTS NAMES
c98f35635625 docker_web "flask run --host 0.…" ... 0.0.0.0:5000->5000/tcp docker_web_1
コンテナの中を確認したい場合は、以下の方法で直接ログインすることができます。
code: bash
$ docker exec -it docker_web_1 bash
もしくは次のように実行します。
code: bash
$ FLASK_ENV="development" FLASK_CONFIG="development" \
docker-compose -f docker/development.yml exec web bash
いずれの場合も、ホストのカレントディレクトリがマウントされているディレクトリ /opt/code(イメージの WORKDIR)に行き着きます。
コンテナを解体するには、デーモンとして実行しているときに、以下を実行します。
code: bash
$ FLASK_ENV="development" FLASK_CONFIG="development" docker-compose -f docker/development.yml down
サーバには Running on http://0.0.0.0:5000/ と表示されていることに注目してください。これは、Dockerコンテナが外部との通信にこのネットワークインターフェースを使用しているためです。しかし、ポートがマッピングされているので、ブラウザで http://localhost:5000 または http://0.0.0.0:5000 のどちらかに向かうことができます。
docker-composeの使い方を簡単にするために、 manage.py というスクリプトでラップして、環境変数を自動的に受け取るようにしたいと思います。
code: manage.py
import os
import json
import signal
import subprocess
import click
docker_compose_file = "docker/development.yml"
# Ensure an environment variable exists and has a value
def setenv(variable, default):
os.environvariable = os.getenv(variable, default) setenv("APPLICATION_CONFIG", "development")
# Read configuration from the relative JSON file
config_json_filename = os.getenv("APPLICATION_CONFIG") + ".json"
with open(os.path.join("config", config_json_filename)) as f:
config = json.load(f)
# Convert the config into a usable Python dictionary
for key, value in config.items():
setenv(key, value)
@click.group()
def cli():
pass
@cli.command(context_settings={"ignore_unknown_options": True})
@click.argument("subcommand", nargs=-1, type=click.Path())
def flask(subcommand):
cmdline = "flask" + list(subcommand) try:
p = subprocess.Popen(cmdline)
p.wait()
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
p.wait()
@cli.command(context_settings={"ignore_unknown_options": True})
@click.argument("subcommand", nargs=-1, type=click.Path())
def compose(subcommand):
cmdline = docker_compose_cmdline + list(subcommand)
try:
p = subprocess.Popen(cmdline)
p.wait()
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
p.wait()
if __name__ == "__main__":
cli()
flaskとcomposeの2つの関数が基本的に同じコードであることに気づいたかもしれませんが、データベースを追加したらすぐにcomposeコマンドに変更が必要になることがわかっているので、リファクタリングの誘惑には負けました。
これで、./manage.py compose up -d と ./manage.py compose down を実行すれば、環境変数が自動的にシステムに渡されるようになりました。
Git コミット
このステップで行った変更は、Git コミット で確認することができます。 参考資料
最後に
この最初の投稿はこれで十分です。ゼロから始めて、Flaskプロジェクトの定型的なコードを追加し、フレームワークでどのような環境変数が使われているかを調べ、設定システムと管理スクリプトを追加し、最後にすべてをDockerコンテナで実行しました。次の投稿では、開発用のセットアップに永続的なデータベースを追加する方法と、テスト用に一時的なデータベースを使用する方法を紹介します。もし私の記事が役に立ったら、興味を持ってくれそうな人にシェアしてください。
開発を楽しんでくださいね。
Flask project setup series
日本語訳: Flaskプロジェクトのセットアップ(TDD/Docker/Postgresなど) - Part 1