Chuyển đến nội dung chính
友田 陽大
Flask in production
Python
Flask
Blueprint
アーキテクチャ設計
本番運用
バックエンド

Flaskの大規模アプリ構成:アプリケーションファクトリ(create_app)とBlueprintで循環importを避けて拡張する

Flask 3.1系の大規模アプリ構成を本番品質で設計する実践ガイド。グローバルappと循環importの破綻、create_appアプリケーションファクトリ、extensions.pyの裸の拡張→init_app、Blueprintのurl_prefix・endpoint命名・入れ子・テンプレート/静的ファイル・エラーハンドラ・CLI、from_object+from_prefixed_envの環境別Config、src/レイアウトとYAGNIの分割規律を公式ドキュメントに忠実な実コードで解説します。

Published
Reading time
24 min read
Author
友田 陽大
Chia sẻ
Mục lục

導入:Flask アプリは「機能不足」ではなく「構造不足」で壊れる

Flask の案件が技術的負債に沈むとき、原因はほとんど Flask の機能不足ではありません。構造の欠如です。app = Flask(__name__) を 1 ファイルのトップに書いて始めたアプリが、ルートとモデルとフォームを足していくうちに、ある日 ImportError: cannot import name 'app' from partially initialized module で起動しなくなる——これは Flask を触ったことのある人なら一度は見た光景でしょう。

この破綻は偶然ではなく、構造的な必然です。グローバルな app を中心に据えた瞬間、ビューもモデルも拡張も「app を import する」ことを強いられ、その app 側もビューを import する——依存が円を描き、import 順序という最も脆い前提にアプリ全体がぶら下がります。

本記事は、この破綻を Flask 3.1 系(現行安定版、本稿執筆時点で 3.1.3)の公式ドキュメントに忠実に解消する設計——**アプリケーションファクトリ(create_app)**と Blueprint、そして両者をつなぐ extensions.pyinit_app パターン——を、本番品質で組み上げる深掘りガイドです。これは Flask 本番運用ガイド の §1(アプリ構成)と §3(ファクトリ)を、循環 import の回避と Blueprint の落とし穴という観点から徹底的に展開したスポークにあたります。全体像はまずピラーを参照してください。

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用してきました。エンドポイントが増え、認証・課金・管理画面が別々の関心事として育っていく中で、ここで示すファクトリと Blueprint の規律だけが、コードベースを「変更しやすい状態(ETC)」に保ち続けました。本記事はその実戦の設計判断を、公式仕様に照らして言語化したものです。

💡 この記事で扱うバージョンFlask 3.1 系(最新安定版 3.1.3)を前提とします。Flask 3.1 は Python 3.9 以上が必要です。なお Flask 3.0 で Flask.__version__ は廃止されたため、バージョンを取得したいときは importlib.metadata.version("flask") を使ってください(後述)。本稿のコードは公式ドキュメントのパターンに基づきます。


1. なぜ単一ファイルを超えるのか:グローバル app と循環 import の破綻

設計を語る前に、何が壊れるのかを正確に理解します。Flask の最小形はこうでした。

# hello.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return "Hello, World!"

学習にはこれで十分です。問題は、機能が増えてビューやモデルを別ファイルに切り出した瞬間に起きます。典型的な「壊れ方」を再現します。

# models.py — モデルが app を欲しがる
from app import app   # ← app.py を import

db = ...  # app.config を読んで初期化したい


# app.py — app がビュー(モデルを使う)を欲しがる
from flask import Flask

app = Flask(__name__)

import models   # ← models.py を import(models は app を import する)

app.py を起動すると、import models の行で models.pyfrom app import app を実行します。しかしこの時点で app.py はまだ import models の行を実行中で、モジュールの初期化が完了していません。結果、app という名前がまだ束縛されておらず、ImportError: cannot import name 'app' from partially initialized module 'app'(循環 import)で落ちます。

この破綻には、3 つの独立した症状があります。

症状何が起きるか根本原因
テストで設定を差し替えられないapp は import 時に確定するので、テスト用 DB やテスト設定に切り替える隙がないapp がモジュールトップで即時生成される
複数構成を同時に持てない本番・ステージング・テストで異なる設定のアプリを並行して作れないapp がモジュール内でシングルトン化している
循環 import を誘発するビュー/モデルが app を import し、app 側もそれらを import する依存方向が双方向になっている

⚠️ アンチパターン:「とりあえず動くから」とグローバル app のまま機能を足し続け、循環 import が出たら import 文を関数の中に押し込んで(遅延 import で)その場しのぎをする。これは負債を隠すだけで、依存が円を描いている事実は消えません。次節のアプリケーションファクトリは、依存方向を「上から下」に一方向化することで、この円を構造から断ち切ります。


2. アプリケーションファクトリ:create_app を深掘りする

解決策は アプリケーションファクトリです。app をモジュールトップで生成するのをやめ、関数の中で組み立てて返す。「いつ・どの設定で・どの部品を載せて」アプリを作るかを、呼び出し側が完全に制御できるようになります。

2.1 公式チュートリアルの正準形

Flask 公式チュートリアル(Flaskr)が示すファクトリの正準形がこれです。すべての要素に意味があるので、一行ずつ読み解きます。

# src/myapp/__init__.py
import os

from flask import Flask


def create_app(test_config: dict | None = None) -> Flask:
    # ① instance_relative_config=True で、設定をリポジトリ外の instance/ から読む
    app = Flask(__name__, instance_relative_config=True)

    # ② コード内の安全な既定値(本番では必ず上書きする)
    app.config.from_mapping(
        SECRET_KEY="dev",
        DATABASE=os.path.join(app.instance_path, "myapp.sqlite"),
    )

    if test_config is None:
        # ③ 本番/開発:instance/config.py があれば上書き(無ければ黙ってスキップ)
        app.config.from_pyfile("config.py", silent=True)
    else:
        # ④ テスト:呼び出し側が渡した設定で上書き
        app.config.from_mapping(test_config)

    # ⑤ instance フォルダは Flask が自動生成しない。明示的に作る
    os.makedirs(app.instance_path, exist_ok=True)

    @app.route("/health")
    def health():
        return {"status": "ok"}

    return app
  • instance_relative_config=True:設定ファイルの解決基点を、パッケージ内ではなくインスタンスフォルダapp.instance_path、既定で <project>/instance/)に切り替えます。これにより秘密情報を含む設定をリポジトリ外に置けます(§6.2)。
  • from_mapping(...):コードに直書きできる安全な既定値だけをここに置きます。SECRET_KEY="dev" は開発用のダミーで、本番では必ず上書きされる前提です。
  • ③④ test_config 分岐この分岐がファクトリの心臓です。引数が None(通常起動)なら instance/config.py から本番設定を読み、引数が渡されれば(テスト)その設定で上書きします。テストは「実ファイルを置かずに」設定を注入できます。
  • os.makedirs(app.instance_path, exist_ok=True)Flask はインスタンスフォルダを自動生成しません。SQLite やアップロードをそこに置くなら、ファクトリで明示的に作る必要があります。公式チュートリアルもこの一行を入れています。

起動は flask CLI がファクトリを自動検出します。

flask --app myapp run --debug

2.2 ファクトリの自動検出と引数渡し

flask --app は、対象モジュールに create_app または make_app という名前の関数があれば、それをファクトリとして自動的に呼び出します。引数を渡したいときは、括弧内を Python リテラルとして書けます。

# 引数なしのファクトリを自動検出
flask --app myapp run --debug

# ファクトリに文字列リテラル "dev" を渡す(括弧内は Python リテラルとして解釈)
flask --app 'hello:create_app("dev")' run

この書式は本番の Gunicorn でも同じです。gunicorn 'myapp:create_app()' のようにファクトリを直接指せます。Gunicorn のワーカー数・ProxyFix・Docker 化といった本番デプロイの詳細は 本番デプロイガイド に集約しています。

2.3 なぜファクトリがテスト容易性と複数環境を「同時に」解決するのか

ファクトリの本質的な価値は、アプリの生成を「遅延」させ「パラメータ化」することにあります。

  • テスト容易性create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"}) のように、テストごとにクリーンな設定でアプリを生成できます。pytest の fixture でアプリを作り直せば、テスト間の状態汚染も起きません(テストの詳細は テスト実践ガイド へ)。
  • 複数環境:同じ create_app を、本番は環境変数の設定で、テストは test_config 引数で呼び分けるだけです。環境ごとにコードを分岐させる必要がありません。

💡 ファクトリは「将来の拡張」ではなく「現在の要件」:アプリケーションファクトリを「規模が大きくなったら導入する」ものと捉えるのは誤りです。テスト容易性は最初から必要な要件であり、ファクトリはそれを満たす最小の構造です。小さなアプリでも create_appextensions.py の分離だけは最初から入れる——これが負債を生まない出発点です(§7 の YAGNI 規律と矛盾しません。分割すべきは Blueprint であって、ファクトリの導入は別問題です)。


3. extensions.py パターン:裸の拡張 → init_app で束縛する

ファクトリが循環 import を断ち切る鍵は、拡張オブジェクトの扱い方にあります。Flask-SQLAlchemy や Flask-Migrate のような拡張を、app と一緒に生成してしまうと、再び循環 import の罠に戻ります。公式が示す解法は明快です——拡張を「アプリに束縛されていない裸の状態」で生成し、ファクトリ内で init_app(app) を呼んで初めて束縛する

3.1 二段階初期化の構造

# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()      # この時点では app を知らない
migrate = Migrate()    # この時点では app を知らない
# src/myapp/__init__.py(抜粋)
from flask import Flask

from .extensions import db, migrate


def create_app(test_config: dict | None = None) -> Flask:
    app = Flask(__name__, instance_relative_config=True)
    # ...設定読み込み(§2 / §6)...

    # ここで初めて app に束縛する(二段階初期化の第二段)
    db.init_app(app)
    migrate.init_app(app, db)

    # Blueprint の登録は import を「関数の中」で行う(§4)
    from .blueprints.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix="/api")

    return app

ポイントは、db = SQLAlchemy()extensions.pyアプリと無関係に生成されることです。モデル定義は from myapp.extensions import dbdb を import しますが、この dbapp を一切知らないため、app を import する必要がありません。依存方向が models → extensions の一方向になり、円が消えます。

3.2 なぜこれが循環 import を構造的に解消するのか

依存グラフで見ると、二段階初期化の威力が明確になります。

アプローチ依存方向結果
グローバル appdb = SQLAlchemy(app)models → app → models循環(起動不能)
extensions.pydb.init_app(app)models → extensions__init__ → extensions__init__ → models(関数内 import)一方向(健全)

extensions.py が「アプリにも、モデルにも、ビューにも依存しない最下層」になることで、すべてのモジュールが安心して extensions を import できます。これは依存性逆転の素朴な実装——具体(特定のアプリ)ではなく抽象(裸の拡張オブジェクト)に依存する——でもあります。

3.3 1 つの拡張オブジェクトが複数アプリに再利用できる

この設計には、循環 import の回避以外にもう 1 つ重要な帰結があります。db という単一の拡張オブジェクトが、init_app を通じて複数のアプリに束縛できることです。本番アプリとテストアプリが、それぞれ別の DB 設定で同じ db オブジェクトを使えます。これがファクトリ(§2)と拡張パターンが噛み合う理由です——ファクトリが複数のアプリを作り、各アプリに同じ拡張を init_app で結びつける。

💡 current_app がこの設計を完成させる:ファクトリでアプリを作ると「app をモジュールトップから import できない」状況が生まれます(import 時には存在しないため)。拡張やビューが現在のアプリの設定へアクセスするには、app を引き回す代わりに current_app プロキシを使います。current_app.config["..."] は「いま処理中のアプリ」へ動的に解決されるため、ファクトリ構成と完全に整合します。current_app / g / アプリケーションコンテキストの仕組みは コンテキスト徹底解説 で深掘りしています。永続化層(SQLAlchemy 2.0 の型付き ORM)の設計は SQLAlchemy 2.0 実践ガイド を併読してください。


4. Blueprint 徹底解説:分割の単位と公式仕様の落とし穴

アプリケーションファクトリが「アプリをどう作るか」を解決したら、次は「機能をどう分割するか」です。それが Blueprint です。Blueprint は「ルート・テンプレート・静的ファイル・エラーハンドラ・CLI コマンドをひとまとめにした、登録可能な機能ユニット」です。Blueprint API は Flask 0.7 で追加されました。

4.1 定義と登録:name は URL ではなく endpoint の前綴り

最も重要な誤解から潰します。Blueprint の name(第一引数)は URL を決めません。endpoint(url_for で使う名前)を前綴りします。

# src/myapp/blueprints/simple_page.py
from flask import Blueprint, render_template
from werkzeug.exceptions import abort

# 第1引数 = name(endpoint の前綴り)、第2引数 = import_name
simple_page = Blueprint("simple_page", __name__, template_folder="templates")


@simple_page.route("/<page>")
def show(page):
    return render_template(f"pages/{page}.html")
# create_app 内で登録
from .blueprints.simple_page import simple_page

app.register_blueprint(simple_page)
# URL を前綴りしたいなら url_prefix を登録時に指定する
app.register_blueprint(simple_page, url_prefix="/pages")

ここで url_for がどう変わるかが核心です。

指定endpoint生成される URL(url_prefix="/pages"
url_for("simple_page.show", page="about")simple_page.show/pages/about

namesimple_page)は endpoint に . 区切りで前綴りされ、URL には url_prefix が前綴りされる——この 2 つは別物です。name を変えても URL は変わらず、url_prefix を変えても endpoint は変わりません。この分離を理解していないと、url_forBuildError を出して延々ハマります。

⚠️ アンチパターンBlueprint("/admin", __name__) のように、name に URL パスを書く。name は endpoint の名前空間であって URL ではありません。スラッシュを含む name は不正です。URL を付けたいなら app.register_blueprint(admin, url_prefix="/admin") を使ってください。同様に、url_for("show") のように Blueprint 名を省くと、ビューが別 Blueprint にいるとき解決に失敗します。同一 Blueprint 内なら url_for(".show")(先頭ドット)で相対参照できます。

4.2 Blueprint の入れ子(nesting)

Blueprint は入れ子にできます。親 Blueprint に子を登録し、親をアプリに登録すると、子は親の nameurl_prefix の両方を継承します。

parent = Blueprint("parent", __name__, url_prefix="/parent")
child = Blueprint("child", __name__, url_prefix="/child")


@child.route("/create")
def create():
    return "created"


parent.register_blueprint(child)   # 親に子を登録
app.register_blueprint(parent)     # アプリに親を登録

この結果、endpoint は parent.child.create、URL は /parent/child/create になります。

url_for("parent.child.create")   # → "/parent/child/create"

nameurl_prefix も連結される点に注意してください。subdomain= を使ったサブドメインでの入れ子も同様に機能します。入れ子は「管理画面の中にユーザー管理と請求管理がある」といった階層を素直に表現できますが、深くしすぎると endpoint 名が長大になります。入れ子は 2 段までを目安にし、それ以上は別アプリ(マイクロサービス)への分割を検討するのが現実的です。

4.3 Blueprint ローカルのテンプレートと静的ファイル:落とし穴の宝庫

Blueprint は自前のテンプレート・静的ファイルを持てますが、ここに公式仕様上の落とし穴が集中しています。

テンプレートBlueprint("x", __name__, template_folder="templates") で Blueprint 固有のテンプレートフォルダを追加できます。ただし優先順位に注意が必要です。

  • Blueprint のテンプレートフォルダは、アプリのテンプレートフォルダより低い優先度で追加されます。つまり同じ相対パスのテンプレートがあれば、アプリ側が Blueprint 側を上書きできます(カスタマイズの余地を残す設計)。
  • 複数の Blueprint が同じ相対パスのテンプレートを提供した場合、最初に登録された Blueprint が勝ちます

⚠️ テンプレート衝突の罠:複数の Blueprint がそれぞれ templates/index.html を持つと、後から登録した Blueprint の index.html永久に解決されません(最初に登録されたものが勝つため)。これを避けるには、Blueprint ごとにサブフォルダで名前空間を切ります——templates/admin/index.htmltemplates/auth/index.html のように。テンプレートの相対パスを Blueprint 名で名前空間化するのが、衝突を防ぐ唯一確実な方法です。

静的ファイル:ここがさらに罠です。Blueprint("admin", __name__, static_folder="static") で静的フォルダを持てますが、url_prefix を持たない Blueprint は、自分の静的フォルダを配信できません

  • Blueprint の静的ファイルは url_prefix + /static で配信され、endpoint は admin.static になります。
  • url_prefix がない Blueprint では、アプリの /static が勝ち、Blueprint の静的フォルダはフォールバックとして検索すらされません
# url_prefix があれば静的フォルダが配信される
admin = Blueprint("admin", __name__, static_folder="static")
app.register_blueprint(admin, url_prefix="/admin")
# → /admin/static/... で配信、endpoint は "admin.static"
# → url_for("admin.static", filename="style.css") → "/admin/static/style.css"

実務では、Blueprint ごとに静的ファイルを散らすより、アプリ単一の /static に集約してビルドツール(Vite 等)でバンドルするほうが運用が単純です。Blueprint ローカル静的は「プラグイン的に完全独立した機能ユニットを配布する」ような特殊な場合に限るのが無難です。

4.4 Blueprint のエラーハンドラと「404 を捕捉できない」例外

Blueprint は @bp.errorhandler(404) でエラーハンドラを登録できますが、ここに最も誤解されやすい落とし穴があります。

@simple_page.errorhandler(404)
def page_not_found(e):
    # これは「無効な URL アクセス」では呼ばれない!
    return render_template("pages/404.html"), 404

Blueprint の errorhandler(404) は、その Blueprint の自身のビュー関数の中で abort(404)raise が起きたときにだけ呼ばれます。 存在しない URL へのアクセス(ルーティング失敗)では呼ばれません。理由は明快で——Blueprint は URL 空間を「所有」していないからです。/nonexistent というリクエストがどの Blueprint に属するべきかを Flask は判断できないため、無効 URL の 404 はアプリレベルのハンドラでしか捕捉できません。

アプリ全体に効くハンドラを Blueprint から登録したいときは、app_errorhandler を使います。

# 自分の Blueprint のビュー内の abort(403) だけを捕捉
@simple_page.errorhandler(403)
def handle_403(e):
    return render_template("pages/403.html"), 403


# アプリ全体の 404(無効 URL を含む)を、この Blueprint から登録する
@simple_page.app_errorhandler(404)
def handle_app_404(e):
    return render_template("404.html"), 404
デコレータスコープ無効 URL の 404 を捕捉?
@bp.errorhandler(404)その Blueprint 自身のビュー内の abort/raise のみ❌ しない
@bp.app_errorhandler(404)アプリ全体(無効 URL を含む)✅ する
@app.errorhandler(404)アプリ全体✅ する

エラーハンドラの解決順序、HTTPException の一括ハンドリング、API での JSON エラー設計の詳細は エラー処理・可観測性ガイド に分けています。

4.5 Blueprint の CLI コマンド

Blueprint は @bp.cli.command(...) で、その Blueprint に紐づく flask CLI コマンドを定義できます。既定では、Blueprint 名を冠したグループの下に入ります。

# blueprints/admin/__init__.py
import click
from flask import Blueprint

admin = Blueprint("admin", __name__)


@admin.cli.command("create")
@click.argument("name")
def create(name):
    """管理ユーザーを作成する。"""
    click.echo(f"created admin: {name}")
# 既定では Blueprint 名(admin)のグループ配下に入る
flask admin create alice

グループ名は変更できます。Blueprint("admin", __name__, cli_group="other") で別名のグループに、cli_group=None でトップレベルに昇格させて flask create alice にできます。db migrate のような独立した運用コマンド群を Blueprint 単位で持たせると、CLI が機能ごとに整理されて運用しやすくなります。


5. 推奨パッケージ構成:src/ レイアウトの具体例

ここまでの設計要素——ファクトリ・extensions.py・Blueprint・Config——を、1 つのディレクトリ構成に落とします。これが本番 Flask の「型」です。src/ レイアウト(パッケージを src/ 配下に置く)を採用すると、テスト時に「インストール済みのパッケージ」として import され、「カレントディレクトリのファイルをうっかり import する」事故を防げます。

myapp/
├── pyproject.toml
├── src/
│   └── myapp/
│       ├── __init__.py          # create_app(アプリケーションファクトリ)§2
│       ├── config.py            # 環境別 Config クラス(Base/Production/Test)§6
│       ├── extensions.py        # db = SQLAlchemy() など「裸」の拡張 §3
│       ├── errors.py            # app_errorhandler でアプリ全体のエラー整形 §4.4
│       ├── models/
│       │   ├── __init__.py
│       │   ├── user.py          # from myapp.extensions import db
│       │   └── invoice.py
│       └── blueprints/
│           ├── __init__.py
│           ├── auth/            # 認証 Blueprint(login/logout/signup)
│           │   ├── __init__.py  # auth = Blueprint("auth", __name__)
│           │   ├── routes.py
│           │   └── templates/auth/   # 名前空間化したテンプレート §4.3
│           └── api/             # API Blueprint(REST エンドポイント)
│               ├── __init__.py  # api = Blueprint("api", __name__)
│               ├── routes.py
│               └── schemas.py   # marshmallow スキーマ(境界バリデーション)
├── instance/                    # .gitignore(秘密・SQLite・アップロード)§6.2
│   └── config.py
├── migrations/                  # Flask-Migrate(Alembic)
└── tests/
    ├── conftest.py              # app / client / runner fixtures
    └── test_*.py

この構成での依存方向を明示します——すべて「上から下」の一方向です。

__init__.py (create_app)
    ├──→ config.py        (Config クラスを読む)
    ├──→ extensions.py    (db, migrate を init_app)
    ├──→ blueprints/*     (register_blueprint)
    └──→ errors.py        (app_errorhandler を登録)

blueprints/api/routes.py
    ├──→ extensions.py    (db を使う)
    ├──→ models/*         (モデルを使う)
    └──→ blueprints/api/schemas.py

models/*  ──→ extensions.py   (db.Model を継承するためだけに db を import)

extensions.py が最下層にいて、誰も __init__.pycreate_app)を import しない——この形を保つ限り、循環 import は構造的に発生しません。API Blueprint の schemas.py における「Router → Schema(境界)→ Model の層分離」は、marshmallow × Flask × SQLAlchemy で本番 REST API を設計する で詳説しています。本記事の Blueprint が「どこに」を、その記事が「中で何を」を担います。

💡 models/blueprints/ をパッケージ(ディレクトリ+ __init__.py)にする:単一ファイルの models.py で始めても問題ありませんが、モデルが増えたら models/ パッケージに昇格させ、models/__init__.py で各モデルを再エクスポートします。こうすると from myapp.models import User という import パスを安定させたまま、内部のファイル分割を自由に変えられます。import パスの安定性は、リファクタの自由度に直結します。


6. 環境別 Config:from_object + from_prefixed_env の二段構え

ファクトリの test_config 分岐(§2.1)と組み合わせて、環境ごとに設定を切り替える仕組みを整えます。本番で揺るがしてはならない原則は 1 つ——秘密情報(SECRET_KEY・DB パスワード・API キー)をコードに書かない。これは 12-factor の核であり、セキュリティの最低ラインです。

6.1 Config クラスの継承階層

app.config.from_object(...) は、大文字の属性だけを設定として読み込みます。これを使い、Base → Production / Test の継承で環境差分を表現します。

# src/myapp/config.py
class BaseConfig:
    """全環境共通の安全な既定値。"""
    JSON_SORT_KEYS = False
    SESSION_COOKIE_HTTPONLY = True       # 既定 True だが意図を明示
    SESSION_COOKIE_SAMESITE = "Lax"
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfig(BaseConfig):
    SESSION_COOKIE_SECURE = True         # HTTPS 限定 Cookie


class TestConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
# create_app 内(§2 の設定読み込み部分を置き換える)
def create_app(test_config: dict | None = None) -> Flask:
    app = Flask(__name__, instance_relative_config=True)

    # ① Config クラスで安全な既定値を与える
    app.config.from_object("myapp.config.ProductionConfig")
    # ② 環境変数で本番値を上書きする(FLASK_ 前綴り)
    app.config.from_prefixed_env()

    if test_config is not None:
        app.config.from_mapping(test_config)   # テストは最後に上書き

    db.init_app(app)
    # ...Blueprint 登録...
    return app

⚠️ from_object の罠:クラス参照は「インスタンス化されない」from_object("myapp.config.ProductionConfig") は、クラスの大文字属性をそのまま読むだけで、クラスをインスタンス化しません。つまり @property で計算した設定値は読み込まれません(property オブジェクトのまま無視される)。動的に計算した設定が必要なら、クラスではなくインスタンスを渡すfrom_object(ProductionConfig()))か、from_prefixed_env() で環境変数から流し込んでください。

6.2 from_prefixed_env とインスタンスフォルダ

from_prefixed_env()Flask 3.0 で追加された、コンテナ/12-factor 時代に最適なメソッドです。FLASK_ で始まる環境変数を自動で読み込み、値は json.loads で型付けします。

# 本番の環境変数(コンテナの Secrets から注入)
export FLASK_SECRET_KEY='...'                       # python -c 'import secrets; print(secrets.token_hex())'
export FLASK_SQLALCHEMY_DATABASE_URI='postgresql+psycopg://...'
export FLASK_MAX_CONTENT_LENGTH=10485760            # json.loads されて int になる
export FLASK_SQLALCHEMY_ENGINE_OPTIONS__pool_size=10  # __ でネストしたキーに代入

FLASK_MAX_CONTENT_LENGTH=10485760 は文字列ではなく int 10485760 として読み込まれ、__(二重アンダースコア)でネストした辞書キーにも代入できます。「環境変数は全部文字列で扱いづらい」という典型的な悩みを Flask 側が解消します。

設定の読み込み口は他にもあります。本記事で使う from_object / from_prefixed_env / from_pyfile のほか、JSON/TOML を読む from_file(path, load=tomllib.load, text=False)、単一の環境変数からパスを読む from_envvar があります。設定管理の全体像は Flask 本番運用ガイド の §4 にまとまっています。

インスタンスフォルダinstance/、Flask 0.8 で追加)は、instance_relative_config=True(§2.1)のときに from_pyfile("config.py") の解決基点になる「リポジトリにコミットしない設定置き場」です。instance/必ず .gitignore に入れてください。Cookie・CSRF・SECRET_KEY 周りのセキュリティ設定の深掘りは セキュリティ実装ガイド を参照してください。

6.3 Flask のバージョンを取得する

設定やヘルスチェックでフレームワークのバージョンを出したくなることがあります。Flask 3.0 で Flask.__version__ は廃止されました。代わりに標準ライブラリの importlib.metadata を使います。

from importlib.metadata import version

flask_version = version("flask")   # "3.1.3" のような文字列

7. いつ分割するか:「3 つ目で抽出」の YAGNI 規律

ここまで Blueprint と src レイアウトを説明してきましたが、最も重要なのは「いつ分割するか」の判断です。設計力とは、構造を増やす力ではなく、構造を増やすべき瞬間を見極める力です。

最初から blueprints/auth/blueprints/api/blueprints/admin/ と細かく割る必要はありません。これは典型的な早すぎる抽象化で、まだ存在しない要件のために認知負荷を先払いする行為です。筆者が本番で守ってきた規律はシンプルです。

段階構造判断
1 機能目create_app 内に直接ルートを書くBlueprint はまだ不要
2 機能目1 つの Blueprint にまとめる「分けたくなる」が、まだ我慢
3 機能目関心事ごとに Blueprint へ抽出するここで初めて分割

この「3 つ目で抽出」は、DRY の「2 回は偶然、3 回はパターン」と同じ哲学です。2 つの似たルートは偶然の一致かもしれませんが、3 つ揃えば「これは独立した関心事だ」という確信が持てます。確信が持てるまで分割を遅らせることで、間違った境界線で割ってしまう(後で割り直す羽目になる)リスクを避けられます。

💡 分割の単位は「変更理由(SRP)」で決める:Blueprint を「URL のプレフィックス」で割るのではなく、「何が変わったら一緒に変わるか」で割ってください。認証ロジックが変わるとき一緒に変わるものは auth/ に、API のスキーマが変わるとき一緒に変わるものは api/ に。同じ理由で変わるコードを 1 つの Blueprint に集め、違う理由で変わるコードを分ける——これが単一責任原則の、Blueprint 粒度での適用です。URL は結果であって、分割の基準ではありません。

ただし例外があります。create_appextensions.py の分離だけは、機能が 1 つでも最初から入れてください(§2.3)。これは「将来の拡張のため」ではなく「テスト容易性という現在の要件」を満たすための分離だからです。YAGNI が適用されるのは Blueprint の細分化であって、ファクトリの導入ではありません。この区別を取り違えないことが、過小設計(テストできない)と過剰設計(割りすぎ)の両方を避ける鍵です。


まとめ:循環 import は「設計」で構造的に消す

大規模 Flask アプリの破綻は、Flask の機能不足ではなく構造不足から来ます。本記事の規律を再掲します。

  1. グローバル app を捨てる。モジュールトップの app = Flask(__name__) は、テスト不能・単一構成・循環 import という三大破綻の根源。
  2. **アプリケーションファクトリ(create_app(test_config=None))**を背骨にする。instance_relative_config=Truetest_config 分岐で、テスト容易性と複数環境を同時に解決する。
  3. 拡張は extensions.py で裸に生成し、init_app(app) で束縛するextensions を最下層に置くことで依存を一方向化し、循環 import を構造から消す。1 つの拡張が複数アプリに再利用できる。
  4. Blueprint で機能を分割するname は endpoint の前綴りで URL ではない、無効 URL の 404 は Blueprint の errorhandler では捕捉できず app_errorhandler が要る、url_prefix のない Blueprint は静的フォルダを配信できない、テンプレートは最初に登録したものが勝つ——この公式仕様の落とし穴を踏まない。
  5. **環境別 Config は from_object(既定値)→ from_prefixed_env(環境上書き)**の二段構え。from_object はクラスをインスタンス化しない点に注意し、秘密はインスタンスフォルダと環境変数でコードから追い出す。
  6. 分割は「3 つ目で抽出」の YAGNI 規律で。Blueprint の細分化は遅らせてよいが、create_appextensions.py の分離だけは最初から入れる。

これらは「規模が大きくなったら導入する」ものではなく、最初の数百行から効く設計です。Flask は「核だけ」を提供し、構造はあなたに委ねます。その自由に責任を持つ最小の規律が、アプリケーションファクトリと Blueprint です。全体像と各論(コンテキスト・テスト・デプロイ・セキュリティ・可観測性)の地図は Flask 本番運用ガイド に戻って俯瞰してください。

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc