マルチテナンシー

1 つのシステムで複数のテナント (顧客) のデータを安全に分離して提供するアーキテクチャ

アーキテクチャクラウド

マルチテナンシーとは

マルチテナンシーは、1 つのシステムで複数のテナント (顧客) のデータを安全に分離して提供するアーキテクチャである。SaaS では標準的な設計で、テナントごとに環境を建てずに空き容量を共有することで、テナント 1 件あたりの固定費と運用対象の数を抑えることが狙いになる。その代償として、分離はインフラの境界ではなくアプリケーションとデータ設計の中に作り込むことになり、1 箇所のテナント ID の付け忘れが他社データの露出に直結する。

分離モデル

分離の粒度は 3 段階で整理できる。表のコストは、テナント数に比例して増えるリソースがどれだけあるかを表している。

モデル分離レベルコスト
サイロテナントごとに独立リソーステナントごとに DynamoDB テーブル
プール全テナントが共有リソース1 つの DynamoDB テーブルに全テナント
ブリッジ一部共有、一部分離共有 Lambda + テナント別テーブル

リレーショナルデータベースに当てはめると、サイロはテナントごとに別インスタンスまたは別データベース、ブリッジは同一データベース内でテナントごとにスキーマを分ける形、プールは共通テーブルに tenant_id 列を持たせる形になる。プールで行レベルセキュリティ (PostgreSQL の Row-Level Security など) を使う場合は、テーブル所有者と superuser、BYPASSRLS 属性を持つロールがポリシーを素通りする点に注意が必要である。アプリケーションが所有者ロールで接続していると、ポリシーを書いたつもりで全テナントの行が見える状態になる。所有者にも適用させるには ALTER TABLE ... FORCE ROW LEVEL SECURITY を明示する。

DynamoDB でのテナント分離 (プールモデル)

プールモデルでは、パーティションキーの先頭にテナント ID を埋め込むことで、キー条件だけで他テナントの行に届かない構造にする。テナント ID を属性として持たせてフィルタで絞る形にすると、条件の付け忘れがそのまま全テナント参照になるため、キーに埋める設計が要になる。

// パーティションキーにテナント ID を含める
await db.put({
  TableName: 'items',
  Item: {
    PK: `TENANT#${tenantId}`,
    SK: `ITEM#${itemId}`,
    data: { ... },
  },
});

// テナントのデータのみ取得
const items = await db.query({
  TableName: 'items',
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: { ':pk': `TENANT#${tenantId}` },
});

この構造の上で、IAM ポリシーの Condition にテナント ID を書き込むと、アプリケーションのコードが誤っても他テナントの行を読めなくなる。DynamoDB では dynamodb:LeadingKeys 条件キーがテーブルの先頭キー属性 (パーティションキー) を表し、この条件キーを使うときは ForAllValues 修飾子が必須になる。実装上は、テナントごとに固定のポリシーを持つ利用者を作るのではなく、認証後にテナント ID を埋めたセッションポリシー付きでロールを引き受け、そのテナント限定の一時認証情報でデータへアクセスする形が扱いやすい。

ノイジーネイバー問題

共有リソースでは、あるテナントの負荷が他テナントの応答時間に漏れ出す。DynamoDB で効いてくるのは、テーブル全体の容量とは別に 1 パーティションあたりの上限があることで、各パーティションは毎秒 3,000 読み込みユニットと毎秒 1,000 書き込みユニットを上限として設計されている。テナント ID をパーティションキーにする設計は分離には効くが、その分 1 テナントの負荷が 1 パーティションに集まるため、巨大テナントはこの上限に先に当たる。

テナント A: 大量のリクエスト  DynamoDB のスロットリング
テナント B: 通常のリクエスト  テナント A の影響で遅延
対策説明
テナントごとのレート制限API Gateway の Usage Plan
DynamoDB のパーティション設計テナント ID をパーティションキーに
Lambda の同時実行制限予約同時実行数は関数単位の設定であり、テナント別に上限を分けたいなら関数自体を分けるか、キューを挟んで取り出し側で絞る。予約は上限と同時に下限としても働き、確保した分は他の関数から使えなくなる

テナント識別

テナントの識別には、JWT のクレームに tenant_id を含める方法、tenant-a.example.com のようなサブドメイン方式、/api/tenants/{tenantId}/ のようなパスプレフィックス方式、テナントごとに API キーを発行する方法がある。

テナント ID をどこから取るかは分離の強度と直結する。パスやクエリの値をそのまま信じると利用者が書き換えられるため、認証済みトークンのクレームを唯一の出所とし、経路上の値は照合にしか使わない形が原則になる。設計の全体像は関連書籍で体系的に追える。

この記事は役に立ちましたか?

関連用語

関連する記事