マルチテナントFastAPIは「スキーマ分離 vs DB分離」の判断から始まる
SaaSやB2Bプロダクトを運用する企業にとって、マルチテナントアーキテクチャは避けて通れない設計課題です。2026年現在、FastAPI 0.115系とSQLAlchemy 2.0系を組み合わせて、1つのアプリケーションコードベースから複数テナント(複数顧客・複数事業)を運用するのが国内外の事実上の標準になりつつあります。しかし、Web上の情報は「テナントごとにDBを分ける例」と「シャーディング例」と「RLSの例」が断片的に散らばっており、実運用で何を選ぶべきかの判断軸が見えづらい状態です。
本記事では、renueが自社のAIバックエンド(FastAPI + MySQL + SQLAlchemy + Alembic + Celery + Azure App Service構成)を複数テナント運用する中で確立した設計パターンを、匿名化して共有します。テナント分離の3戦略の選び方、SessionManagerの具体的な実装、複数データベース横断クエリの罠、Alembicマイグレーション運用、認証・ロギング・CI/CD整備、本番稼働後に襲ってくる7つの落とし穴まで、実装粒度で整理しました。FastAPIでマルチテナントSaaSを真剣に設計している開発者向けの、実装寄りの技術記事です。
関連記事として生成AI受託開発の費用相場ガイド、AI内製化 vs 外注、Function Calling完全ガイド、MCP完全ガイドもご参照ください。
テナント分離の3戦略:何を選ぶべきか
戦略A:シャードDB・シャードスキーマ(最も弱い分離)
1つのデータベース・1つのスキーマに全テナントのデータを入れ、すべてのテーブルに `tenant_id` カラムを持たせて論理分離する方式です。実装コストは最小ですが、誤ったクエリで別テナントのデータが見えるリスクが常に残ります。テナント数が数万〜数十万規模になる消費者向けSaaSや、テナント間の機能差がほぼないシンプルなBtoBプロダクトに向きます。
向く場面:テナント数が極めて多い、テナント間の機能差が小さい、開発チームが小規模。
向かない場面:テナント間でスキーマを独自拡張したい、規制業界、データ分離要件が厳しい、テナント単位でパフォーマンス特性が大きく異なる。
戦略B:シャードDB・別スキーマ(PostgreSQL Schemaや同等機能)
1つのデータベースインスタンス内で、テナントごとに独立したスキーマを持つ方式です。PostgreSQLのschemaやMySQLの論理DB分離、SQL Serverのschemaを活用します。物理的には1インスタンスで済むのでコスト効率が良く、論理的にはテナントが独立しているため、誤クエリによる事故リスクが低減します。
課題:Alembicマイグレーションをテナント数分繰り返す必要があり、マイグレーション運用が複雑になります。テナント追加時の自動化が必須です。
向く場面:中〜大規模SaaS、テナントごとにスキーマ拡張は不要、コスト効率重視。
戦略C:別DB・別スキーマ(最も強い分離)
テナントごとに独立したデータベース(MySQL 8.0・PostgreSQL・SQL Server等)を用意する方式です。データ分離が最も強く、テナント単位でバックアップ・復元・パフォーマンスチューニング・法規制対応(データ主権・地理的配置)が独立して実施できます。代わりにインフラコスト、接続プール管理、クロスDBクエリの扱いに工夫が必要です。
向く場面:エンタープライズB2B、規制業界(金融・医療・公共)、テナントごとに機能差が大きい、データ主権要件がある、テナントあたりの取引量が大きい。
向かない場面:テナント数が数万を超える(接続数が破綻する)、インフラコスト最小化が最優先。
renueの実運用では、B2B向けのAIエージェントバックエンドでは戦略Cを採用しています。規制業界や機密性の高いワークロードを扱うテナントが含まれるため、物理分離による信頼性を優先しました。一方で、テナント数が数万〜数十万規模になる消費者向けプロダクトでは戦略Aまたは戦略B+Row Level Security(RLS)併用が現実解です。
SessionManagerの実装パターン(戦略C向け)
戦略C(別DB・別スキーマ)を採る場合、SQLAlchemyのSessionを「テナントごとに別のエンジン・別のSessionFactory」で管理する必要があります。renueでは以下のパターンを採用しています。
実装の骨格
- TenantSessionManagerクラス:`dict[str, sessionmaker]` 型のキャッシュを内部に持ち、テナントIDをキーにsessionmakerを遅延生成します。初回アクセス時にengineを作成し、2回目以降はキャッシュを返すことで、接続プール初期化コストを最小化します。
- DB URLの動的生成:`db_host + "/" + tenant_id` のように、ホストは共通・DB名はテナントIDから決定する形式にします。これにより、テナント追加時は新規DB作成とマイグレーション適用だけで済みます。
- テスト環境の分離:pytestの並列実行(pytest-xdist)でテナント間のテストが衝突しないよう、環境変数 `PYTEST_XDIST_WORKER` をDB名のsuffixに含めます。これによりワーカー別にテストDBが分離されます。
- クラウドDB接続のSSL対応:接続URLに `.azure.com` や `.cloudprovider.com` が含まれる場合は自動的にSSL設定を適用する判定ロジックを入れます。オンプレMySQL(SSL無し)とクラウドMySQL(SSL必須)を同じコードで扱えます。
接続プールの推奨パラメータ
renueの本番運用で長期安定稼働しているパラメータ例です。ワークロード特性に応じて調整が必要ですが、スタート地点として参考になります。
| パラメータ | 推奨値 | 理由 |
|---|---|---|
| pool_size | 50 | 長時間接続を保持、急増時にmax_overflowに逃げる |
| max_overflow | 50 | 一時的スパイクに対応、pool_sizeの100%が目安 |
| pool_timeout | 300(5分) | 重いバッチ処理や長クエリに耐える |
| pool_pre_ping | True | クラウドDBの不意な切断対応で必須 |
| pool_recycle | 3600(1時間) | MySQL wait_timeoutの手前で再接続 |
| echo | False | 本番では必ずFalse、メモリとI/O節約 |
| expire_on_commit | False | commit後にインスタンスを再利用可能に |
| autocommit | False | 明示トランザクション推奨 |
| autoflush | False | 不要なflushを抑制 |
特に `pool_pre_ping=True` と `pool_recycle=3600` は、Azure MySQL・AWS RDS・GCP Cloud SQL等のマネージドMySQLで本番稼働させるなら必須です。これを外すと「夜中に勝手に切断されて翌朝エラー」という事故が定期発生します。
複数データベース横断の罠
マルチテナントSaaSの多くは「メインDBは自前クラウド、一部テーブルは別クラウドやマネージドサービスに格納」といった構成を取ることが多いです。例えば「メイン機能はAzure MySQL、議事録や動画データはGoogle Cloud SQL」のようなケースです。この時、SQLAlchemyのSessionは「1セッション=1接続先」という前提で動くため、単純にJOINで横断すると失敗します。
典型的な症状と解決パターン
renueの実運用で実際に遭遇した典型ケース: Google Cloud SQLの議事録テーブルを、Azure MySQLのプロジェクトテーブルで絞り込みたい、という横断クエリを書こうとするとSQLAlchemyのセッションエラーまたは0件が返される、という症状でした。原因は、Google Cloud SQL向けのSessionからAzure MySQLのテーブルを参照しようとしていたためです。
解決策は、それぞれのDBに対して別の `get_db_session()` を呼び出し、一度片方のDBからID/絞り込みキーを取得してから、もう片方のDBにそれを使ってクエリする「アプリケーション層でのjoin」に置き換える方法です。具体的には以下の手順です。
- 絞り込みキー(例:プロジェクト名)をAzure MySQL側のSessionから取得
- そのキーを使ってGoogle Cloud SQL側のSessionから絞り込みクエリを発行
- 結果をアプリケーション層で結合
- try/finally でそれぞれのSessionを確実にcloseする
SQLレベルでのjoinに比べてパフォーマンスは劣りますが、マルチDB構成ではこれが現実的な解です。クロスDBのjoinをどうしても高速化したい場合は、ETLで定期的にデータをミラーリングするか、federated queryサービス(BigQuery、Trino、PrestoDB等)を併用します。
ディレクトリ構造のベストプラクティス
renueでは以下の構造をマルチテナントFastAPIプロジェクトの標準形として採用しています。
src/
├── main.py # FastAPIアプリ起動・ルーター登録
├── setting.py # 環境変数・設定管理
├── shared/ # 全テナント共通の機能
│ ├── db/ # DB接続・Session管理
│ ├── lib/ # 共通ライブラリ(AI、Storage、暗号化等)
│ ├── middlewares/ # 認証・CORS・ロギング
│ ├── migrations/ # Alembicマイグレーション
│ ├── models/ # 共通SQLAlchemyモデル
│ ├── repositories/ # データアクセス層
│ ├── routers/ # 共通APIエンドポイント
│ └── schemas/ # Pydanticスキーマ
└── tenants/ # テナント固有の実装
├── tenant_a/
│ ├── models/ # テナント固有モデル
│ ├── repositories/
│ ├── routers/ # テナント固有API
│ └── schemas/
└── tenant_b/
├── models/
└── ...
このレイアウトの要点は、(1) 共通ロジックを `shared/` に集約、(2) テナント固有実装を `tenants/
テナント別ルーターの登録
`main.py` でFastAPIのAPP起動時に、全テナントのルーターをprefix付きで登録します。例えば `/api/shared/*` は共通機能、`/api/tenant_a/*` はテナントA固有機能、という形です。テナント追加時は新しいディレクトリを作り、`main.py` にルーター登録を追記するだけでスケールします。
Alembicマイグレーション運用
マルチテナントで最も運用が重くなるのがマイグレーションです。renueでは以下のポリシーを採用しています。
- 共通マイグレーションと個別マイグレーションを分離:`shared/migrations/` にはすべてのテナントに適用する共通マイグレーション、`tenants/
/migrations/` にはテナント固有のマイグレーションを配置。 - テナント追加のスクリプト化:新規テナント作成時は「DB作成 → 共通マイグレーション適用 → 個別マイグレーション適用 → seedデータ投入」を1つのスクリプトで実行できる形にします。
- 本番マイグレーションは環境別スクリプト経由:`./scripts/migrate_databases.sh prod` のようなラッパースクリプトを作り、本番URLと認証情報は環境変数から読む形にします。手動で `alembic upgrade head` を本番に直打ちしないことで事故を防ぎます。
- CI/CDでマイグレーション整合性を検証:PR段階で「マイグレーションが綺麗に上がるか・下がるか」を自動チェックします。これを怠るとマージ後に本番デプロイが壊れます。
renue本番運用9原則
原則1:テナント分離戦略は最初の設計で決める
戦略A/B/Cの選択を後から変えるのは事実上不可能です。規制業界やエンタープライズB2Bなら戦略C、コンシューマ向けSaaSなら戦略AまたはBを迷わず選びます。
原則2:SessionManagerは単一のクラスに集約する
散らばった `create_engine` 呼び出しが複数ファイルにある状態は運用の地獄です。`TenantSessionManager` のようなクラスを1つ作り、engine生成・プール設定・SSL判定・テストDB切替をすべて集約します。
原則3:接続プール設定は本番前にチューニング不可避
`pool_size`、`max_overflow`、`pool_timeout`、`pool_pre_ping`、`pool_recycle` の5つは本番前に必ず決めます。デフォルト値のままだと、高負荷時の接続枯渇、夜間の自動切断、長時間クエリのタイムアウト等で事故が起きます。
原則4:クロスDBクエリはアプリケーション層で結合する
SQL層でのJOINは諦め、片方のDBでキーを取得 → もう片方のDBで絞り込み、という順序で書きます。一見冗長ですが、テナント分離・責務明確化の両面でメリットがあります。
原則5:テナント間の直接参照を禁止する
`tenants/tenant_a/` から `tenants/tenant_b/` のモデルをimportしない規律を絶対に守ります。共有が必要なものは `shared/` に昇格させます。
原則6:マイグレーションは環境別スクリプト経由
本番への `alembic upgrade head` 直打ちを禁止し、環境別のラッパースクリプト経由に統一します。人的ミスによる事故を構造的に防げます。
原則7:pre-PRチェックをスクリプト化する
`rye lock --universal`、`requirements.txt` 同期、`black` フォーマット、lintなどを1つの `scripts/pre-pr-check.sh` に集約し、PR前に必ず実行する規律にします。これがないと「CI失敗→再コミット→再CI」のループで生産性が落ちます。
原則8:自動デプロイをmainブランチマージに紐付ける
mainへのPRマージで自動デプロイされるGitHub Actionsを設定し、手動デプロイを緊急時のみに限定します。手動デプロイが日常化している組織は、設定ファイルのdriftや人的ミスで事故が増えます。
原則9:環境別設定とDEVモード認証スキップを分離
ローカル開発時は `IS_AUTH_REQUIRED=false` で認証スキップを許容しつつ、`PY_ENV=production` では**絶対に**認証バイパスが効かないようガードします。この二重ガードがないと、開発フラグが本番に漏れ出す事故が起きます。
本番稼働後に襲ってくる7つの落とし穴
落とし穴1:夜間の自動切断(pool_pre_ping忘れ)
マネージドMySQLは一定時間アイドル接続を切断します。`pool_pre_ping=True` がないと翌朝大量のエラーで気付きます。
落とし穴2:接続数の枯渇(pool_sizeとmax_overflowの読み違い)
テナント数×pool_sizeがMySQLの`max_connections`を超えると接続枯渇します。クラウドMySQLの `max_connections` は意外と小さい(200〜500程度)ので、pool_sizeを大きく取る場合は事前にMySQL側の上限も引き上げる必要があります。
落とし穴3:クロスDBクエリでの0件返却
複数DB構成で「なぜか0件」が返るのは、多くの場合セッション管理の誤りです。`get_db_session()` がどちらのDBを指しているか、明示的に意識する必要があります。
落とし穴4:Alembicマイグレーションの順序依存
テナント追加時に共通マイグレーションと個別マイグレーションの適用順序を間違えると、初期テーブルが作られない状態でアプリが起動して500エラーを返します。
落とし穴5:ローカルARM64ビルドと本番AMD64の不整合
MacのM1/M2で `docker build` したイメージをそのままAMD64環境(Azure App Service等)にpushすると `exec format error` で起動しません。解決策はACR buildやGitHub Actions側でビルドする、またはmulti-arch buildを明示することです。
落とし穴6:DBマイグレーションなしでのデプロイ
CI/CDパイプラインに「マイグレーション適用」ステップがないと、コード側は新カラムを前提にしているのに本番DBが旧スキーマのまま、という事故が起きます。
落とし穴7:テストDBの並列実行衝突
pytest-xdistで並列テストを走らせる時、DB名が固定だと複数ワーカーが同じDBを同時に触り、フレーク(不安定)テストが量産されます。`PYTEST_XDIST_WORKER` を使ったワーカー別DB分離が必須です。
FAQ
Q1. 戦略Aから戦略Cへ後から移行できますか?
理論的には可能ですが、現実的にはコストが大きく非常に困難です。データ移行、外部キー整合性、運用フロー変更、バックアップ戦略変更など、最初から戦略Cで設計した場合に比べて10倍以上のコストがかかることも珍しくありません。最初の設計で決め切るのが鉄則です。
Q2. PostgreSQL Row Level Security(RLS)は戦略Aの代替になりますか?
はい、一定程度。RLSは「全テナントのデータを1テーブルに入れつつ、SQLレベルで他テナント行を見えなくする」仕組みで、戦略AのSQL誤りリスクを低減します。ただし設定ミスによる事故リスクは残るため、エンタープライズ要件では戦略C(別DB)が無難です。
Q3. FastAPIのdependency injectionでテナント解決する方法は?
middleware + Dependsを組み合わせます。JWT trailerからテナントIDを取り出すdependency(例:`get_current_tenant`)を書き、router関数の引数で `tenant: Tenant = Depends(get_current_tenant)` と書くことで、テナントコンテキストを自動注入できます。
Q4. MySQL 8.0とPostgreSQLはどちらを選ぶべきですか?
テナント分離の強度を重視するならPostgreSQL(schema/RLS対応が強力)、エンタープライズ採用率・運用ツール充実・MySQL互換性を重視するならMySQL 8.0です。renueはAzure MySQL 8.0を中核に使っていますが、新規プロジェクトではPostgreSQLも選択肢に入れる価値があります。
Q5. テナントごとのパフォーマンスチューニングはどうする?
戦略Cなら各テナントのDBインスタンスを個別にチューニング可能です。戦略A/Bだと全テナントに影響するため、個別最適化はアプリケーション層のキャッシュやクエリ最適化で対応するのが現実解です。
Q6. Celeryのタスクはテナント別にキューを分けるべきですか?
重要度の高いテナント(契約金額が大きい、SLAが厳しい)は専用キューに分けるのが推奨です。全テナント共用キューだと、1テナントの重いバッチが他テナントのレスポンスを遅延させます。
Q7. マルチテナントSaaSでAIエージェントを動かす時の注意点は?
(1) LLM API呼出のコストをテナント別に配賦、(2) プロンプトやRAGベクトル空間をテナント別に分離、(3) ハルシネーションやガードレール発動率もテナント別に計測、(4) MCPサーバー接続の権限もテナント別、の4点が最低ラインです。詳細はLLMOps実践ガイドをご参照ください。
Q8. この設計パターンはNextjsフロントと組み合わせられますか?
はい。`/api/tenant_a/*` のようなpath prefix設計にしておけば、Next.js側からは通常のAPIコールと変わりません。認証トークンにテナントIDを埋め込んでおけば、フロント側は意識する必要がありません。
まとめ:マルチテナントFastAPIは「最初の戦略決定が9割」
2026年のマルチテナントFastAPI設計は、戦略A/B/Cの選択が後の運用負荷と事故率を決定的に左右します。規制業界やエンタープライズB2Bなら迷わず戦略C(別DB別スキーマ)、コンシューマSaaSなら戦略AまたはB。SessionManagerを1クラスに集約、接続プール設定を本番前にチューニング、クロスDBクエリをアプリケーション層で結合、Alembicマイグレーションを環境別スクリプト経由で運用、pre-PRチェックとCI/CD自動デプロイを標準装備にすることで、長期安定運用が可能になります。
renueは、複数のAIエージェント事業を内製で立ち上げ、FastAPI + MySQL + SQLAlchemy + Celery + Azure構成で本番運用してきた実体験から、マルチテナントバックエンド設計・運用の伴走支援を提供しています。「戦略A/B/Cの選択を相談したい」「既存の戦略Aから移行する道筋を知りたい」「pool設定を本番前にレビューしてほしい」などのご相談をお受けしています。
renueにマルチテナントFastAPI設計・運用の相談をする
renueは、複数AIエージェント事業の内製運用実績から、マルチテナントFastAPIバックエンドの設計・実装・運用ガイダンスを提供しています。テナント分離戦略の選定、SessionManagerの実装レビュー、接続プール設定、マイグレーション運用、CI/CD整備、本番落とし穴の事前回避まで、実装粒度で伴走します。
