renue

ARTICLE

マルチテナントFastAPIバックエンド設計2026|テナント分離3戦略・SessionManager実装・本番運用9原則

公開日: 2026/4/7

マルチテナント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_size50長時間接続を保持、急増時にmax_overflowに逃げる
max_overflow50一時的スパイクに対応、pool_sizeの100%が目安
pool_timeout300(5分)重いバッチ処理や長クエリに耐える
pool_pre_pingTrueクラウドDBの不意な切断対応で必須
pool_recycle3600(1時間)MySQL wait_timeoutの手前で再接続
echoFalse本番では必ずFalse、メモリとI/O節約
expire_on_commitFalsecommit後にインスタンスを再利用可能に
autocommitFalse明示トランザクション推奨
autoflushFalse不要な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」に置き換える方法です。具体的には以下の手順です。

  1. 絞り込みキー(例:プロジェクト名)をAzure MySQL側のSessionから取得
  2. そのキーを使ってGoogle Cloud SQL側のSessionから絞り込みクエリを発行
  3. 結果をアプリケーション層で結合
  4. 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//` 配下に完全分離、(3) 共通ライブラリは全テナントから参照OK、(4) テナント間の参照は禁止(tenant_aからtenant_bのモデルをimportしない)、という境界線を明確にすることです。

テナント別ルーターの登録

`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整備、本番落とし穴の事前回避まで、実装粒度で伴走します。

無料相談はこちら

関連記事