メインコンテンツまでスキップ

テナントカスタマイズパターン

このドキュメントの目的

テナント別設定管理では、設定駆動アーキテクチャによりコード変更なしでテナントの挙動を変える方法を学びました。しかし、設定のパラメータ化だけでは対応できないケースがあります。

本記事では、設定駆動の限界を超えてテナント固有の要件に対応するための拡張パターンを学びます。

具体例について: 本記事ではIDサービスを題材にした例(認証フロー、外部IdP連携等)を使用していますが、カスタマイズパターン自体はSaaS全般に共通です。


設定駆動ではカバーできない要件

どんなケースか

設定駆動は「同じロジックの中のパラメータを変える」アプローチです。以下のような要件は、パラメータの変更では吸収できません。

要件の種類なぜ設定では無理か
ロジック自体が異なるテナントAは独自の承認フローを持つif/elseの分岐ではなく、処理の流れ自体が違う
外部連携が異なるテナントBは独自の本人確認APIと連携接続先URLだけでなく、リクエスト/レスポンスの形式が異なる
データモデルの拡張テナントCはユーザーに独自属性を追加したいスキーマ自体の変更が必要
UIフローの変更テナントDは登録ステップの順序を変えたい画面遷移のロジックが異なる

対応方針の全体像

テナント固有の要件

├─ パラメータの違い ──────> 設定駆動で対応(tenant-configuration.md)

├─ 処理の分岐・拡張 ────> 拡張パターンで対応(本記事)
│ │
│ ├─ 処理の前後に追加 ───────> Webhook / Event Hook
│ ├─ 処理の一部を差し替え ──> Plugin / Extension Point
│ ├─ 条件付きロジック ───────> ルールエンジン
│ └─ データの柔軟な拡張 ────> カスタム属性

└─ 根本的に異なる ──────> テナント別デプロイ(Silo化)

パターン1: Webhook / Event Hook

概要

処理の特定のタイミングで、テナントが指定した外部URLにHTTPリクエストを送信するパターンです。SaaS側のコードを変更せずに、テナントが自分のサーバーで任意の処理を実行できます。

SaaS側の処理フロー:

ユーザー登録リクエスト


┌──────────────┐
│ 1. 入力検証 │
└──────┬───────┘


┌──────────────┐ ┌──────────────────────┐
│ 2. Webhook │────>│ テナントのサーバー │
│ 送信 │ │ │
│ (pre-hook) │<────│ ・追加検証 │
└──────┬───────┘ │ ・外部システム連携 │
│ │ ・承認/拒否の判定 │
▼ └──────────────────────┘
┌──────────────┐
│ 3. ユーザー │ ← Webhookの応答に基づいて続行 or 中断
│ 作成 │
└──────┬───────┘


┌──────────────┐ ┌──────────────────────┐
│ 4. Webhook │────>│ テナントのサーバー │
│ 送信 │ │ │
│ (post-hook) │ │ ・後続処理の実行 │
└──────────────┘ │ ・通知、ログ記録 │
└──────────────────────┘

Hookの種類

種類タイミングSaaS側の挙動ユースケース
Pre-hook処理の前応答を待ち、結果に基づいて続行/中断追加バリデーション、承認フロー
Post-hook処理の後非同期で通知(応答を待たない)ログ記録、外部同期、通知
Transform-hook処理の途中応答でデータを変換して続行データ加工、属性マッピング

設計上の考慮事項

タイムアウトとフォールバック:

Webhook呼び出し

├─ 成功(200 OK)────────> 応答を使って処理続行

├─ タイムアウト(3秒)───> フォールバック動作
│ ・デフォルト値で続行?
│ ・処理を中断?
│ ・リトライ?

└─ エラー(5xx)─────────> フォールバック動作
考慮事項推奨
タイムアウトPre-hook: 3〜5秒、Post-hook: 10秒
リトライPost-hook: 最大3回(Exponential Backoff)、Pre-hook: リトライしない
フォールバックテナント設定で「Webhook失敗時の挙動」を選択可能にする
認証Webhook URLへのリクエストにHMAC署名を付与し、テナント側で検証
べき等性リトライに備え、同じイベントIDで重複呼び出しを判定可能にする

メリット・デメリット

メリットデメリット
SaaS側のコード変更が不要テナント側に開発力が必要
テナントが自由にロジックを実装可能レイテンシーが増加(外部HTTP呼び出し)
疎結合(SaaSとテナントが独立)Webhook先の障害がSaaSに影響しうる

パターン2: Plugin / Extension Point

概要

処理の一部をインターフェースとして定義し、テナントごとに異なる実装を差し込めるようにするパターンです。

Extension Pointの構造:

共通処理フロー:
┌──────────┐ ┌──────────────────┐ ┌──────────┐
│ 前処理 │──>│ Extension Point │──>│ 後処理 │
└──────────┘ │ │ └──────────┘
│ ┌──────────────┐ │
│ │ デフォルト実装 │ │ ← 設定なしのテナント
│ ├──────────────┤ │
│ │ Tenant A実装 │ │ ← テナントAの独自ロジック
│ ├──────────────┤ │
│ │ Tenant B実装 │ │ ← テナントBの独自ロジック
│ └──────────────┘ │
└──────────────────┘

Extension Pointの設計例

典型的なExtension Point:

┌─────────────────────────────────────────────────┐
│ 処理フロー Extension Point │
├─────────────────────────────────────────────────┤
│ ユーザー登録 ・登録前バリデーション │
│ ・属性変換 │
│ ・登録後処理 │
├─────────────────────────────────────────────────┤
│ 認証 ・追加認証ステップ │
│ ・認証結果の判定 │
│ ・リスク評価 │
├─────────────────────────────────────────────────┤
│ トークン発行 ・カスタムクレーム追加 │
│ ・トークン形式の変換 │
├─────────────────────────────────────────────────┤
│ 通知 ・通知チャネル選択 │
│ ・テンプレート選択 │
└─────────────────────────────────────────────────┘

実装アプローチ

Extension Pointの実装方法は、テナントがどこまで自由度を持つかによって異なります。

アプローチ1: 内蔵プラグイン選択型

SaaS側があらかじめ複数の実装を用意し、テナントが設定で選択します。

SaaS側が用意する実装:

通知チャネル:
├── email(デフォルト)
├── slack
├── teams
└── custom-webhook

テナント設定:
{ "notification_channel": "slack" }
  • テナントの自由度: 低(用意されたものから選ぶだけ)
  • SaaS側の負担: 高(実装を用意する必要がある)
  • セキュリティリスク: 低(SaaS側で管理されたコード)

アプローチ2: テナント提供のコード実行型

テナントがスクリプトやロジックを登録し、SaaS側のサンドボックスで実行します。

テナントが登録するスクリプト:

function onUserRegistration(user) {
// 独自のバリデーション
if (!user.email.endsWith("@company-a.com")) {
return { allow: false, reason: "社内メールのみ許可" };
}
return { allow: true };
}

実行環境:
┌───────────────────────────┐
│ サンドボックス │
│ ・CPU制限: 100ms │
│ ・メモリ制限: 64MB │
│ ・ネットワーク: 不可 │
│ ・ファイルシステム: 不可 │
└───────────────────────────┘
  • テナントの自由度: 高(任意のロジックを実装可能)
  • SaaS側の負担: 高(サンドボックス環境の構築・維持)
  • セキュリティリスク: 高(テナントコード実行にはサンドボックスが必須)

Webhook vs Plugin の使い分け

観点WebhookPlugin
テナントの開発場所テナントの外部サーバーSaaS内(設定または登録コード)
レイテンシー高い(HTTP往復)低い(インプロセス実行)
信頼性テナントサーバーに依存SaaS側で制御可能
自由度最大(テナントが自由に実装)制約あり(サンドボックス内)
適用場面非同期処理、外部連携低レイテンシーが必要な同期処理

パターン3: ルールエンジン

概要

テナント管理者が条件とアクションのルールを定義し、ロジックを動的に制御するパターンです。コードを書かずに、設定UIで条件付きの振る舞いを定義できます。

ルール定義の例:

ルール1: "高リスクログインにはMFAを強制"
条件: login_country != tenant_country AND device_trust == "unknown"
アクション: require_mfa

ルール2: "営業時間外のアクセスを制限"
条件: current_hour < 8 OR current_hour > 20
アクション: require_additional_verification

ルール3: "大量データエクスポートに承認を要求"
条件: export_record_count > 10000
アクション: require_admin_approval

ルール構造

ルールの構成要素:

┌──────────────────────────────────────────────────┐
│ ルール │
│ │
│ 名前: "高リスクログイン検知" │
│ 優先度: 100(高いほど先に評価) │
│ 有効: true │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 条件 (When) │───>│ アクション │ │
│ │ │ │ (Then) │ │
│ │ ・属性比較 │ │ ・MFA強制 │ │
│ │ ・論理演算 │ │ ・通知送信 │ │
│ │ ・パターン │ │ ・ブロック │ │
│ │ マッチ │ │ ・ログ記録 │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘

ルール評価の戦略

戦略説明適用場面
First Match最初にマッチしたルールのアクションを実行排他的なルール(1つだけ適用)
All Matchマッチした全ルールのアクションを実行累積的なルール(複数適用可能)
Priority Order優先度順に評価し、最高優先度のマッチを適用競合するルールがある場合

メリット・デメリット

メリットデメリット
テナント管理者がUIで定義可能(コード不要)複雑なルールの管理が困難
柔軟な条件定義ルール間の競合・矛盾の検出が必要
即座に反映(デプロイ不要)パフォーマンスへの影響(ルール数に比例)

パターン4: カスタム属性

概要

テナントがデータモデルに独自の属性を追加できるようにするパターンです。テナントごとにDBスキーマを変更するのではなく、柔軟なデータ構造で対応します。

実装方式

方式1: JSONカラム

users テーブル:

id │ tenant_id │ name │ email │ custom_attributes
────┼───────────┼────────┼────────────────┼──────────────────────────
1 │ tenant-a │ 田中 │ tanaka@a.com │ {"department": "営業", "employee_id": "A001"}
2 │ tenant-b │ 鈴木 │ suzuki@b.com │ {"role_level": 3, "region": "関東"}
  • メリット: スキーマ変更不要、柔軟
  • デメリット: JSONカラムへのインデックスが限定的、型安全性が低い

方式2: EAV(Entity-Attribute-Value)

user_attributes テーブル:

user_id │ tenant_id │ attribute_key │ attribute_value
────────┼───────────┼───────────────┼────────────────
1 │ tenant-a │ department │ 営業
1 │ tenant-a │ employee_id │ A001
2 │ tenant-b │ role_level │ 3
2 │ tenant-b │ region │ 関東
  • メリット: 属性ごとのクエリが可能、属性定義の管理が容易
  • デメリット: JOINが多くパフォーマンスが低下、型の表現が弱い

方式比較:

観点JSONカラムEAV
柔軟性高い高い
クエリ性能JSONインデックスに依存JOINコスト
型安全性低い(JSON内は型なし)低い(文字列格納が多い)
スキーマ管理不要属性定義テーブルで管理可能
適用場面属性の検索が少ない場合属性の検索・フィルタが多い場合

カスタム属性のバリデーション

テナントが自由に属性を追加できる場合でも、属性定義にスキーマを設けてバリデーションを行います。

属性定義(テナントが登録):

{
"attribute_key": "department",
"display_name": "部署",
"type": "string",
"required": true,
"max_length": 100,
"allowed_values": ["営業", "開発", "人事", "経理"]
}

バリデーション:
・型チェック: 値が定義された型と一致するか
・必須チェック: required属性が欠落していないか
・値の制約: 許可された値リストに含まれるか

パターンの選択指針

判断フローチャート

テナント固有の要件がある

├─ パラメータの違いだけ?
│ └─ Yes ──> 設定駆動(tenant-configuration.md)

├─ 処理の前後に追加したい?
│ └─ Yes ──> Webhook / Event Hook

├─ 処理の一部を差し替えたい?
│ └─ Yes ──> Plugin / Extension Point

├─ 条件付きで挙動を変えたい?
│ └─ Yes ──> ルールエンジン

├─ データ項目を追加したい?
│ └─ Yes ──> カスタム属性

└─ 上記のどれでも対応できない?
└─ テナント別デプロイ(後述)

パターン比較

パターンテナントの自由度SaaS側の実装コストレイテンシー影響適用場面
Webhook最大大(HTTP往復)非同期処理、外部連携
Plugin中〜高同期処理の差し替え
ルールエンジン小〜中条件付きロジック
カスタム属性データモデルの拡張
テナント別デプロイ最大最大なし根本的に異なる要件

テナント別デプロイ(最終手段)

いつ必要か

上記の拡張パターンでも対応できない場合、テナント専用のインスタンスを用意します。

通常のマルチテナント(Pool):

┌──────────────────────────────┐
│ 共有インスタンス │
│ │
│ Tenant A, B, C, D, ... N │
└──────────────────────────────┘

テナント別デプロイ(Silo化):

┌────────────────┐ ┌──────────────────────────────┐
│ Tenant X専用 │ │ 共有インスタンス │
│ インスタンス │ │ │
│ │ │ Tenant A, B, C, D, ... N │
│ ・独自コード │ └──────────────────────────────┘
│ ・独自設定 │
│ ・専用DB │
└────────────────┘

Silo化の判断基準

判断基準Pool維持Silo化検討
カスタマイズ量設定+数個のWebhookコードフォークが必要なレベル
SLA要件共有SLAで十分専用SLA(99.99%保証等)
データ要件共有DBで対応可能専用DB・専用リージョン必須
収益規模標準プラン専用環境のコストを正当化できる収益
コンプライアンス標準的な要件業界固有の規制(金融、医療等)

Silo化のコスト

Silo化は最大の柔軟性を提供しますが、運用コストも最大です。

Poolモデルのコスト:
インフラ: 共有
デプロイ: 1回で全テナント
監視: 1セットの監視
パッチ適用: 1回

Silo化後のコスト:
インフラ: 共有 + Tenant X専用
デプロイ: 共有(1回) + Tenant X用(1回)
監視: 共有(1セット) + Tenant X用(1セット)
パッチ適用: 共有(1回) + Tenant X用(1回)

判断の原則: Silo化は「最後の手段」です。まずWebhook、Plugin、ルールエンジンで対応できないか検討し、それでも無理な場合にのみSilo化を選択します。


組み合わせの実践例

実際のSaaSでは、これらのパターンを組み合わせて使います。

典型的なマルチテナントSaaSのカスタマイズ構成:

┌──────────────────────────────────────────────┐
│ 全テナント共通 │
│ │
│ 設定駆動: パラメータ(セッション、MFA等) │
│ Feature Flags: 機能ON/OFF │
├──────────────────────────────────────────────┤
│ Standard以上のテナント │
│ │
│ Webhook: イベント通知、外部連携 │
│ カスタム属性: ユーザー属性の拡張 │
├──────────────────────────────────────────────┤
│ Enterpriseテナント │
│ │
│ ルールエンジン: 条件付きポリシー │
│ Plugin: 認証フローのカスタマイズ │
├──────────────────────────────────────────────┤
│ 超大規模テナント(例外的) │
│ │
│ テナント別デプロイ │
└──────────────────────────────────────────────┘

まとめ

学んだこと

  • 設定駆動で対応できない要件には、Webhook、Plugin、ルールエンジン、カスタム属性の4つの拡張パターンがある
  • Webhookは疎結合で最も導入が容易だが、レイテンシーが課題
  • Pluginは低レイテンシーだが、サンドボックス等のセキュリティ対策が必要
  • ルールエンジンはテナント管理者がUIで定義可能だが、ルールの複雑化に注意
  • カスタム属性はJSONカラムかEAVで実装し、属性定義のバリデーションが重要
  • テナント別デプロイは最終手段であり、運用コストが大きい
  • 実践では複数のパターンをプラン別に組み合わせて使う

次のステップ

  1. マルチテナントセキュリティ - セキュリティ上の考慮事項
  2. テナント別設定管理 - 設定駆動の基礎に戻る

最終更新: 2026-03-03 対象: SaaSアプリケーション開発者・アーキテクト