導入:Flask は「小さい」のではなく「核だけ」である
Flask は「小さなフレームワーク」と紹介されがちですが、その理解は本番運用では危険です。正確には、Flask は WSGI アプリケーションの核(ルーティング・リクエスト/レスポンス・テンプレート・設定・コンテキスト)だけを提供し、ORM もフォームも認証も内蔵しない、という設計思想のフレームワークです。公式が「micro」と呼ぶのは「機能が貧弱」という意味ではなく、「何を載せるかをあなたが決める」 という意味です。
この思想は両刃です。要件に最小フィットした構成を組めば、FastAPI や Django より薄く・速く・読みやすいバックエンドになります。一方で、核しかないがゆえに、構造(アプリケーションファクトリ・Blueprint・設定・コンテキスト・デプロイ)を自分で設計しなければ、小さなアプリがそのまま「グローバル変数とimport順序に支配されたレガシー」へ滑り落ちます。Flask 案件の失敗のほとんどは、Flask の機能不足ではなく、この構造設計の欠如から来ます。
本記事は、その構造を Flask 3.1 系(現行安定版)の公式ドキュメントに忠実に、本番品質で組み上げるためのピラー(全体地図)です。筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用してきました。ここで示すのは、その実戦で必要だった設計だけです。
💡 この記事で扱うバージョン:Flask 3.1 系(本稿執筆時点の最新安定版は 3.1.3、2026-02 リリース)を前提とします。Flask 3.1 は Python 3.9 以上が必要で、依存に Werkzeug(WSGI/HTTP 層)・Jinja(テンプレート)・MarkupSafe・ItsDangerous(署名)・Click(CLI)・Blinker(シグナル) を持ちます。本稿のコードは公式ドキュメントのパターンに基づきます。各論は本ピラーから個別のスポーク記事へリンクします。
1. 全体像:Flask アプリの「7 つの設計対象」
本番の Flask アプリで設計判断が必要になるのは、実は次の 7 つだけです。本ピラーはこの 7 つを俯瞰し、それぞれの深掘りを専用記事に渡します。
| # | 設計対象 | 中心 API / 概念 | 深掘り記事 |
|---|---|---|---|
| 1 | アプリ構成 | create_app / Blueprint / init_app | 大規模構成ガイド |
| 2 | 設定管理 | config.from_* / インスタンスフォルダ | 本記事 §4 |
| 3 | コンテキスト | current_app / g / request / session | コンテキスト徹底解説 |
| 4 | エラー処理・ログ | errorhandler / dictConfig | エラー処理・可観測性ガイド |
| 5 | セキュリティ | SECRET_KEY / Cookie / CSRF / エスケープ | セキュリティ実装ガイド |
| 6 | デプロイ | Gunicorn / ProxyFix / Docker | 本番デプロイガイド |
| 7 | テスト | test_client / pytest fixtures | テスト実践ガイド |
そして、Flask を採用すべきか(FastAPI / Django との比較)という上流の意思決定は 技術選定ガイド にまとめています。
2. 最小の Flask:まず「WSGI アプリ」であることを掴む
設計の話に入る前に、Flask が何であるかを 1 ファイルで確認します。公式クイックスタートの最小形です。
# hello.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, World!"
開発サーバーで起動します。
flask --app hello run --debug
ここで押さえるべきは 2 点です。
appは WSGI アプリケーションである。公式の言葉を借りれば「Flask は WSGI アプリケーションであり、WSGI サーバーがそれを動かす」。つまりflask runの開発サーバーや本番の Gunicorn は 「サーバー」 であって、Flask 本体ではありません。この分離が、後述するデプロイ設計の出発点です。--debugは開発専用。公式は「開発サーバーを本番にデプロイしてはならない。安全でも、安定でも、効率的でもない」と明確に警告しています。本番起動は §8 で扱う Gunicorn です。
⚠️ アンチパターン:
if __name__ == "__main__": app.run()をエントリポイントにして、それを本番でそのまま起動する。app.run()は開発サーバーであり、本番では Gunicorn 等の WSGI サーバーからappオブジェクトを読み込ませます。app.run()は必ずif __name__ == "__main__":ブロックに置き、本番経路から隔離してください。
この最小形は学習には良いものの、グローバルな app をモジュールトップに置く点が、本番ではそのまま技術的負債になります。次節でそれを解消します。
3. アプリケーションファクトリ:本番 Flask の背骨
3.1 なぜグローバル app を捨てるのか
app = Flask(__name__) をモジュールトップに書くと、3 つの問題が構造的に発生します。
- テストで設定を差し替えられない:
appは import 時に確定するので、テスト用 DB やテスト用設定に切り替える隙がない。 - 複数構成を持てない:本番・ステージング・テストで異なる設定のアプリを同時に作れない。
- 循環 import を誘発する:ビューやモデルが
appを import し、app側もそれらを import するため、依存が循環する。
解決策が アプリケーションファクトリです。app を関数の中で組み立てて返す。これにより「いつ・どの設定で・どの部品を載せて」アプリを作るかを呼び出し側が制御できます。公式チュートリアルの正準形がこれです。
# 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", # 本番では必ず上書き(§5・§7)
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 フォルダは自動生成されない。明示的に作る
os.makedirs(app.instance_path, exist_ok=True)
@app.route("/health")
def health():
return {"status": "ok"}
return app
起動も flask CLI がファクトリを自動検出します。
flask --app myapp run --debug
💡 ファクトリの自動検出:
flask --appは、対象モジュールにcreate_appまたはmake_appという名前の関数があればファクトリとして自動的に呼び出します。引数を渡したいときはflask --app 'myapp:create_app("dev")' runのように、括弧内を Python リテラルとして書けます。本番の Gunicorn でもgunicorn 'myapp:create_app()'と同じ書式が使えます(§8)。
3.2 拡張は「未束縛 → init_app で束縛」
Flask-SQLAlchemy のような拡張も、ファクトリと整合させます。拡張オブジェクトはモジュールトップで app 抜きに生成し、ファクトリ内で init_app(app) を呼んで束縛します。これで 1 つの拡張オブジェクトが複数アプリ(本番・テスト)に再利用でき、循環 import も避けられます。
# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# src/myapp/__init__.py(抜粋)
from .extensions import db, migrate
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
# ...設定読み込み...
db.init_app(app) # ここで初めて app に束縛
migrate.init_app(app, db)
from .blueprints.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix="/api")
return app
このパターンと、循環 import を避けるパッケージ構成、Blueprint の入れ子・CLI コマンドまでの深掘りは、大規模構成ガイド に分けています。
4. 設定管理:秘密情報をコードから追い出す
app.config は「大文字のキーだけを意味のある設定として扱う」辞書サブクラスです。本番では コードに秘密情報(SECRET_KEY・DB パスワード・API キー)を書かない——これは 12-factor の核であり、セキュリティの最低ラインです。Flask は複数の読み込み口を用意しています。
| メソッド | 用途 | 例 |
|---|---|---|
from_mapping(**kw) | コード内のデフォルト値 | app.config.from_mapping(SECRET_KEY="dev") |
from_object(obj) | 環境別 Config クラス | app.config.from_object("myapp.config.Production") |
from_pyfile(path, silent=True) | インスタンスフォルダの Python 設定 | app.config.from_pyfile("config.py", silent=True) |
from_prefixed_env() | 環境変数(FLASK_ 前綴り) | app.config.from_prefixed_env() |
from_file(path, load=...) | JSON / TOML 設定 | app.config.from_file("config.toml", load=tomllib.load, text=False) |
4.1 推奨:from_object(既定値)→ from_prefixed_env(環境上書き)
筆者が本番で使う基本形は、「Config クラスで安全な既定値を与え、環境変数で本番値を上書きする」 二段構えです。from_prefixed_env() は Flask 3.0 で追加された、コンテナ/12-factor 時代に最適なメソッドで、FLASK_ で始まる環境変数を自動で読み込み、値は json.loads で型付けします。
# src/myapp/config.py
class BaseConfig:
JSON_SORT_KEYS = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
class ProductionConfig(BaseConfig):
SESSION_COOKIE_SECURE = True # HTTPS 限定 Cookie(§7)
class TestConfig(BaseConfig):
TESTING = True
# create_app 内
app.config.from_object("myapp.config.ProductionConfig")
app.config.from_prefixed_env() # FLASK_SECRET_KEY, FLASK_SQLALCHEMY_DATABASE_URI ...
# 本番の環境変数(コンテナの 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 になる
💡
from_prefixed_envの威力:値がjson.loadsで解釈されるため、FLASK_MAX_CONTENT_LENGTH=10485760は文字列ではなくint 10485760として読み込まれます。__(二重アンダースコア)でネストしたキーにも代入できます。「環境変数は全部文字列で扱いづらい」という典型的な悩みを、Flask 側が解消してくれます。
4.2 インスタンスフォルダ:リポジトリ外の設定置き場
Flask(__name__, instance_relative_config=True) を指定すると、app.instance_path(既定で <project>/instance/)が「リポジトリにコミットしない設定・SQLite・アップロードの置き場」になります。from_pyfile("config.py") はこの instance/ からの相対で解決されます。instance/ は必ず .gitignore に入れてください。
⚠️
DEBUGは設定ファイルに書かない。公式は「DEBUGをコードや設定で有効化すると期待通りに動かないことがある」と注意しています。デバッグはFLASK_DEBUG=1環境変数かflask run --debugで、開発時のみ有効にします。本番でDEBUG=Trueはインタラクティブデバッガを晒す重大な脆弱性です。
4.3 Flask 3.1 で増えた「本番で効く」設定
Flask 3.1 系では、本番の堅牢化に直結する設定が追加されています。詳細は各スポークで扱いますが、ピラーとして一覧します。
| 設定キー | 既定値 | 追加 | 効果 |
|---|---|---|---|
SECRET_KEY_FALLBACKS | None | 3.1 | 鍵ローテーション(旧鍵で署名検証を継続) |
SESSION_COOKIE_PARTITIONED | False | 3.1 | CHIPS(パーティション化 Cookie)。有効化で SECURE も強制 |
MAX_FORM_MEMORY_SIZE | 500_000 | 3.1 | 非ファイルのフォーム値サイズ上限(DoS 緩和) |
MAX_FORM_PARTS | 1_000 | 3.1 | フォームのパート数上限(DoS 緩和) |
TRUSTED_HOSTS | None | 3.1 | ルーティング時に Host ヘッダを検証(Host ヘッダ攻撃対策) |
5. コンテキスト:current_app と g で「引き回さない」
ファクトリでアプリを作ると、「app をどこからも import できない」状況が生まれます(import 時には存在しないため)。Flask はこれを コンテキストローカルで解決します。
current_app:いま処理中のアプリへのプロキシ。appを import せずに設定や拡張へ届く。g:いまのコンテキスト(≒1 リクエスト)の間だけ生きる名前空間。DB 接続のキャッシュなどに使う。request/session:いまのリクエスト/セッション。
公式が示す典型は「リクエスト中は同じ DB 接続を使い回し、終了時に閉じる」パターンです。
from flask import g, current_app
def get_db():
if "db" not in g: # リクエスト内で初回だけ接続
g.db = connect_to_database(current_app.config["DATABASE"])
return g.db
@app.teardown_appcontext
def teardown_db(exception): # コンテキスト終了時に必ず閉じる
db = g.pop("db", None)
if db is not None:
db.close()
💡
gは「グローバル変数」ではない。公式は明言しています——「gの "g" は global だが、それはコンテキスト内でグローバルという意味。データはコンテキスト終了で失われ、リクエストをまたぐ保存には使えない。リクエスト間の保存にはsessionか DB を使う」。さらに重要な事実として、Flask のコンテキストローカルは Python のcontextvarsと Werkzeug のLocalProxyで実装されており、単なるスレッドローカルではありません。これがasyncビューでも正しく機能する理由です。
コンテキストの押し込み/取り出しの仕組み、_get_current_object()、teardown_appcontext と teardown_request の違い、バックグラウンドタスクで請求書のように context を持ち出す copy_current_request_context、RuntimeError: Working outside of application context の正しい対処までは、コンテキスト徹底解説 で深掘りします。
6. エラー処理とロギング:本番で「沈黙しない」
6.1 エラーハンドラ:HTML ではなく JSON を返す API 設計
REST API では、例外を HTML エラーページではなく構造化 JSON で返すのが定石です。Flask は @app.errorhandler で例外クラス/ステータスコードごとにハンドラを登録できます。
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_http_exception(e: HTTPException):
"""すべての HTTP エラーを JSON で返す。"""
return jsonify(code=e.code, name=e.name, description=e.description), e.code or 500
@app.errorhandler(404)
def not_found(e):
return jsonify(error="resource not found"), 404
ビューからは abort(404, description="...") でエラーを送出します。ハンドラの解決順序(コード → クラス階層 → 最も具体的なもの)、Blueprint 側ハンドラの優先と「Blueprint は 404 を捕捉できない」例外、InternalServerError.original_exception の扱いは、エラー処理・可観測性ガイド に分けています。
6.2 ロギング:dictConfig を「アプリ生成より前に」
Flask のロギングは標準 logging そのものです。公式の最重要注意は 「ロギングはアプリ生成より前に設定せよ」——app.logger を設定前に触ると既定ハンドラが付いてしまうためです。正準形は logging.config.dictConfig です。
from logging.config import dictConfig
dictConfig({
"version": 1,
"formatters": {"default": {
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
}},
"handlers": {"wsgi": {
"class": "logging.StreamHandler",
"stream": "ext://flask.logging.wsgi_errors_stream",
"formatter": "default",
}},
"root": {"level": "INFO", "handlers": ["wsgi"]},
})
app = Flask(__name__) # ← dictConfig の後で生成する
本番では、これを JSON 構造化ログ + リクエスト ID に拡張します(RequestFormatter で request.url 等を注入)。可観測性(相関 ID・Sentry・ヘルスチェック)の設計は同じくエラー処理・可観測性ガイドへ。
7. セキュリティ:境界を「設定」で固める
Flask の「micro」は、セキュリティでも「賢い既定 + あなたの選択」です。本番で最低限固める境界を挙げます(深掘りはセキュリティ実装ガイド)。
SECRET_KEYを必ず設定する。Flask のsessionはクライアント側の署名付き Cookie(ItsDangerous で署名)です。改ざんは検知できますが、SECRET_KEYが漏れる・弱いと署名を偽造されます。生成はpython -c 'import secrets; print(secrets.token_hex())'。- Cookie 属性を固める。
SESSION_COOKIE_HTTPONLY(既定True)、SESSION_COOKIE_SECURE(既定False→本番はTrue)、SESSION_COOKIE_SAMESITE("Lax"推奨)。 - CSRF は内蔵されていない。公式は「フォーム検証フレームワークは Flask に存在しない」と明言。CSRF 対策は Flask-WTF の
CSRFProtectで導入します。 - XSS は Jinja の自動エスケープが既定。
.html/.htm/.xml/.xhtmlテンプレートは自動エスケープされます。escape/Markupはmarkupsafeから import します(flaskからではない)。 - セキュリティヘッダは既定で付かない。HSTS・CSP・
X-Content-Type-Options: nosniff・X-Frame-Optionsは自分で付けるか、Flask-Talisman に任せます。
# 本番の Cookie 既定(ProductionConfig 等で)
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
8. デプロイ:開発サーバーを捨て、Gunicorn で動かす
本番では flask run(開発サーバー)を使いません。Flask は WSGI アプリなので、WSGI サーバー が app を読み込んで動かします。Linux の定番は Gunicorn です。
# app オブジェクトを直接指す場合
gunicorn -w 4 'myapp:app'
# アプリケーションファクトリの場合(§3)
gunicorn -w 4 'myapp:create_app()'
-w(ワーカー数):公式の出発点はCPU × 2。既定は 1 ワーカーで、本番には不足します。- リバースプロキシの背後では
ProxyFix。nginx / ALB の背後でX-Forwarded-*を信頼するには、Werkzeug のProxyFixを噛ませます。信頼するプロキシ段数を正しく設定しないと、クライアントが偽のX-Forwarded-Forを送れてしまうため、ここは慎重に。
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
ワーカー種別(同期 vs gevent)の選び方、Docker のマルチステージ・非 root・ヘルスチェック、グレースフルシャットダウン、TRUSTED_HOSTS/SERVER_NAME の扱いは、本番デプロイガイド に集約しています。
💡
async defビューの誤解:Flask は 2.0 からasync defビューに対応(pip install flask[async])しますが、公式は明確に注意しています——「各リクエストは依然 1 ワーカーを占有する。async ビューにしても同時に捌けるリクエスト数は変わらない」。async が効くのは「1 ビュー内で複数の外部 API を並行呼び出しする」ような IO 並行であって、スループット向上ではありません。本格的な async が要件なら ASGI 前提の Quart を検討する、というのが公式の立場です。
9. テスト:test_client と pytest fixtures で契約を固定する
Flask はテスト容易性が高いフレームワークです。app.test_client() で実サーバーを立てずにリクエストを往復でき、ファクトリ構成ならテスト専用設定のアプリを毎回作れます。
# tests/conftest.py
import pytest
from myapp import create_app
@pytest.fixture()
def app():
app = create_app({"TESTING": True})
yield app
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def runner(app):
return app.test_cli_runner()
# tests/test_health.py
def test_health(client):
res = client.get("/health")
assert res.status_code == 200
assert res.json == {"status": "ok"}
TESTING=True の意味、session_transaction() でのセッション操作、with client: でのリクエスト後検証、follow_redirects、CLI コマンドのテスト(test_cli_runner)までは、テスト実践ガイド で深掘りします。
10. 推奨パッケージ構成:すべてをつなぐ
ここまでの設計を 1 つのディレクトリ構成にまとめます。これが本番 Flask の「型」です。
myapp/
├── pyproject.toml
├── src/
│ └── myapp/
│ ├── __init__.py # create_app(アプリケーションファクトリ)§3
│ ├── config.py # 環境別 Config クラス §4
│ ├── extensions.py # db = SQLAlchemy() など「裸」の拡張 §3.2
│ ├── logging.py # dictConfig §6.2
│ ├── errors.py # 共通エラーハンドラ §6.1
│ ├── models/ # SQLAlchemy モデル
│ └── blueprints/
│ ├── auth/ # 認証 Blueprint
│ └── api/ # API Blueprint
├── instance/ # .gitignore(秘密・SQLite)§4.2
│ └── config.py
└── tests/
├── conftest.py # app / client / runner fixtures §9
└── test_*.py
💡 「3 つ目で抽出」の原則:最初から
blueprints/を細かく割る必要はありません。1 つのファイルで始め、機能が 2 つ・3 つと増えてから Blueprint に分割する——YAGNI です。ただしcreate_appとextensions.pyの分離だけは最初から入れてください。これは「将来の拡張」ではなく「テスト容易性という現在の要件」を満たすための分離だからです。
まとめ:Flask は「設計の自由」に責任を持つフレームワーク
Flask の本質は 「核だけを提供し、構造はあなたが決める」 ことです。だからこそ、本番品質は次の規律で決まります。
- アプリケーションファクトリ(
create_app)でグローバルappを捨て、テスト容易性と複数環境を同時に得る。 - 設定を
from_prefixed_env+ インスタンスフォルダで 12-factor 化し、秘密をコードから追い出す。 current_app/gでアプリ参照を引き回さず、コンテキストの寿命に資源を縛る。- エラーは JSON で、ログは
dictConfigで、本番で沈黙しないようにする。 SECRET_KEY・安全な Cookie・CSRF・自動エスケープでセキュリティ境界を設定で固める。- 開発サーバーを捨て、Gunicorn + ProxyFixで動かす。
test_client+ pytest fixtures で境界の契約をテストに固定する。
この 7 つは、FastAPI でも Django でも形を変えて必要になる普遍的な設計対象です。Flask はそれを最も薄く、最も明示的に書かせてくれる——それが、適切に設計されたときの Flask の強さです。各論の深掘りは、本ピラーからリンクした各スポーク記事へお進みください。Flask を採用すべきかどうかの判断自体は、Flask vs FastAPI vs Django 技術選定ガイド から始めるのが近道です。