目次


1. 分離モデルを先に決める

最初の大きな設計判断は、テナント同士をどこまで物理的に分けるかです。

モデル構成分離の強さコスト・運用向いているケース
Siloテナントごとに DB(またはアカウントやスタック)を分ける強い高い。マイグレーションも N 回必要規制業種、大口顧客
BridgeDB は共有し、スキーマをテナントごとに分ける中程度中程度。テナント数が数千を超えると厳しい中規模の B2B
Pool全テーブル共有で tenant_id 列で区別する弱いので、アプリ側と DB 側で担保が必要低い。スケールしやすい大多数の SaaS
  • 基本は Pool で始めます。そのうえで、大口テナントだけ Silo に切り出せる「ハイブリッド(ティア別)」構成を取れるようにしておくのが現実的です。
  • どのモデルでも、テナントの所在(どの DB・シャードにいるか)を引けるテナントカタログを持っておきます。これがあれば、後からテナントを移動できます。

2. データ分離(Pool モデルの場合)

  • 全テーブルに tenant_id を持たせる:子テーブルにも冗長に持たせます。JOIN しなくても絞り込めるようにするためです。
  • インデックスと一意制約は tenant_id を先頭にする:例として UNIQUE (tenant_id, email) のようにします。グローバルな一意制約にすると、他テナントのデータが存在することを推測される漏洩経路になります。
  • 複合外部キーを使う:(tenant_id, parent_id) で参照させると、テナントをまたいだ参照を DB レベルで防げます。
  • ID は UUID か ULID にする:連番 ID だと、ほかのテナントの規模を推測されたり、ID を推測した攻撃を受けたりする余地が残ります。
  • PostgreSQL の RLS(Row Level Security)を多層防御として使う:アプリ側でスコープを付け忘れても、DB 側で止められます。
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

RLS を使うときは、テナントの指定を必ず SET LOCAL(トランザクション単位)で行います。PgBouncer をトランザクションモードで使っている場合、セッション単位の SET は別のリクエストにそのまま残って漏れます。また、アプリが接続する DB ロールはテーブルの owner にも BYPASSRLS にもしないでください。どちらでも RLS が効かなくなります。

3. テナントコンテキストの扱い

  • リクエストの入口で一度だけ解決する:サブドメイン、カスタムドメイン、JWT のクレームなどからテナントを特定します。リクエストボディやクエリパラメータの tenant_id は信用しないでください。
  • 解決したコンテキストをすべての経路に伝播させる
    • バックグラウンドジョブ:エンキュー時に tenant_id を引数に含め、ワーカー側で再設定します。
    • キャッシュ:キーに tenant:{id}: のプレフィックスを付けます。
    • S3:s3://bucket/{tenant_id}/... の形にし、IAM のポリシー条件でもプレフィックスを制限します。
    • 検索インデックスやベクトル DB:フィルタかネームスペースで分けます。RAG で漏れる事故は多いです。
    • WebSocket や Pub/Sub:チャネル名にテナントを含めます。
  • Rails の場合は acts_as_tenant や CurrentAttributes で暗黙的にスコープをかけられます。ただし unscoped や生 SQL、find_by_sql を使うとスコープが外れるので、RLS と組み合わせておくと安心です。

4. 認証・認可

  • ユーザーとテナントは多対多(Membership)で設計します。1 人のユーザーが複数テナントに所属するのは前提として考えます。
  • ロールや権限はテナント単位で付与します。
  • サポート担当などがテナントをまたいでアクセスする機能は、明示的な「なりすまし」機能として作ります。期限付きにし、テナント側の承認を取り、監査ログを残します。
  • エンタープライズ向けには、SSO(SAML/OIDC)や SCIM の設定をテナントごとに持てるようにします。

5. Noisy Neighbor 対策

特定のテナントが大量のリソースを使って、ほかのテナントに影響を出すことへの対策です。

  • テナントごとにレート制限とクォータ(API 呼び出し数、ストレージ、同時実行数)を設けます。
  • ジョブキューはテナント間で公平に処理されるようにします。テナントごとの同時実行上限を設けたり、ラウンドロビンにしたりします。1 テナントの一括インポートでキュー全体が詰まる事故はよく起きます。
  • 重いテナントは、専用のシャードやワーカーに移せる設計にしておきます。

6. 運用・デプロイ

  • マイグレーション:Silo や Bridge では N 回実行することになるので、冪等で再開可能にし、段階的にロールアウトします。後方互換を保つ expand/contract パターンが必須です。
  • テナント単位のフィーチャーフラグと設定:機能の段階リリースや、プランごとの機能差分を出すのに使います。
  • オンボーディングとオフボーディングの自動化:テナント作成、初期データ投入、削除、データエクスポートをコードで実行できるようにしておきます。
  • バックアップと復元:Pool モデルでは「1 テナント分だけ過去の状態に戻す」のが難しいです。要件にあるなら、早い段階で設計しておきます。

7. 可観測性とコスト

  • ログ、メトリクス、トレースのすべてに tenant_id を付けます。障害時の影響範囲の特定や、サポート対応で効いてきます。
  • テナント別のコストを集計できるようにします(AWS のコスト配分タグ、使用量の計測)。価格設計や赤字テナントの発見に必要です。
  • SLO は全体とあわせて、上位テナント単位でも見ます。

8. セキュリティとコンプライアンス

  • 必要に応じて、テナントごとに暗号鍵を分けます(KMS、BYOK)。鍵を破棄することで、そのテナントのデータを確実に消去する手段にもなります。
  • データの保存地域の要件がある場合は、リージョン別にデプロイし、テナントカタログでルーティングします。
  • 監査ログは改ざんできない形で保存し、テナント管理者が閲覧できるようにします。

9. テスト

  • テストデータは常に 2 テナント以上で作ります。「テナント A の認証情報でテナント B のリソースにアクセスすると 404 になる」ことを、全エンドポイントに対して網羅的にテストします。
  • 静的解析や Lint で、スコープなしのクエリを検出します。

よくあるアンチパターン

  • 入力された tenant_id を、そのまま WHERE 句に使う
  • 管理画面やバッチだけ、テナントのスコープを外して実装する
  • キャッシュキーや S3 のパスにテナントが含まれていない
  • テナント固有の要件に、コードの if 分岐で対応する(設定やフラグで対応すべき)
  • テナントの移動や分割を想定しない構成で始めてしまう

0件のページ