リファレンスアーキテクチャ
参考例マルチテナントSaaSプラットフォーム
コントロールプレーンをアプリケーションプレーンから分離し、エッジからデータストアまで、すべての層にテナントの識別情報を引き継ぐSaaSアーキテクチャです。
課題
セルフサービスプランを利用する小規模なチームから、セキュリティ質問票や調達プロセスを伴う大企業まで、多くの組織に販売されるソフトウェアプロダクトを想定します。どの顧客も、自社のデータがほかのすべての顧客から見えないことを期待し、大規模な顧客はさらに、シングルサインオン、監査ログ、予測可能な性能を期待します。
顧客ごとに個別のデプロイで提供する方式は、考え方としては単純ですが、運用とアップグレードのコストが高くなります。すべての顧客を1つの共有デプロイで提供する方式は効率的ですが、分離、リソースの公平な利用、顧客ごとの設定がエンジニアリング上の課題になります。アーキテクチャは、顧客の階層ごとに、この両極の間のどこに位置づけるかを選択しなければなりません。
設計が満たすべき要件
- あるテナントが別のテナントのデータを読み取ったり変更したりできてはならず、この制約はアプリケーションコードより下の層で強制されなければなりません。
- 新しいテナントは、エンジニアが手作業を行うことなく、自動的にプロビジョニングされなければなりません。
- エンタープライズテナントは、自社のIDプロバイダーを利用でき、自動プロビジョニングによってユーザーを管理できなければなりません。
- あるテナントの高負荷な利用が、ほかのテナントの体験を劣化させてはなりません。
- 機能、上限、課金はテナントのプランに基づいて決まり、デプロイなしで変更できなければなりません。
- 利用状況、エラー、性能は、個々のテナントに帰属させて把握できなければなりません。
設計
コンポーネント
テナント対応エッジ
- カスタムドメイン
- テナント解決
- WAF
- テナント別レート制限
アイデンティティ・SSO
- SAML・OIDC
- SCIMプロビジョニング
- ロールと権限
- MFA
コントロールプレーン
テナントを管理し、テナントのトラフィックは処理しない
- テナントライフサイクル
- プランとエンタイトルメント
- 課金とメータリング
- フィーチャーフラグ
アプリケーションプレーン
- ステートレスサービス
- バックグラウンドジョブ
- テナントコンテキスト
- Webhook
データ分離
分離モデルはプラン階層ごとに選択
- 行レベルセキュリティ
- テナント別スキーマ
- 専用データベース
- テナント別の鍵
テナント別オブザーバビリティ
- 利用状況メトリクス
- テナントタグ付きトレース
- ノイジーネイバーアラート
- 監査ログ
テナント対応エッジ
エッジは、サブドメイン、カスタムドメイン、またはトークンから、リクエストがどのテナントに属するかを解決し、帰属を特定できないリクエストを拒否します。テナント単位でレート制限を適用するため、ある顧客のトラフィックの急増はその範囲に封じ込められます。カスタムドメインとその証明書は、自動的に発行および更新されます。
アイデンティティとシングルサインオン
アイデンティティ層は、小規模な顧客向けにはパスワードとMFAによるサインインを、エンタープライズ向けにはSCIMプロビジョニングを伴うSAMLまたはOIDCのフェデレーションをサポートします。ロールと権限はテナントごとに定義されます。発行されるすべてのトークンは、テナント識別子を署名付きクレームとして含み、下流のサービスはクライアントが提供する値の代わりにこれを信頼します。
コントロールプレーン
コントロールプレーンは、サインアップ、プロビジョニング、プラン変更、一時停止、削除というテナントのライフサイクルを担います。プランとエンタイトルメントのカタログを保持し、各テナントが何を利用できるかについての信頼できる情報源となります。アプリケーションプレーンとは別にデプロイし、スケールさせるため、一方のインシデントがもう一方を停止させることはありません。
アプリケーションプレーン
ステートレスなサービスがプロダクトのトラフィックを処理し、各リクエストの開始時に一度だけ、検証済みのトークンからテナントコンテキストを確立します。そのコンテキストは、データベースセッション、バックグラウンドジョブ、キャッシュ、送出するイベントへ伝播されます。ワークキューはパーティション分割されているため、あるテナントのバックログが別のテナントのジョブを滞らせることはありません。
データ分離
標準のテナントは1つのPostgreSQLデータベースを共有し、行レベルセキュリティのポリシーによって、すべてのクエリが現在のテナントに限定されます。より厳格な要件を持つテナントは、ルーティング層の選択によって、専用スキーマまたは専用データベースに配置できます。キャッシュキー、オブジェクトストレージのパス、検索インデックスは、いずれもテナント単位でプレフィックスを付けるかパーティション分割します。
メータリングと課金
サービスは利用イベントを発行し、それらはテナントごとに、プランの価格設定の基準となる数量へ集計されます。課金連携は決済プロバイダーを通じてそれらを請求書に変換し、エンタイトルメントチェックは同じデータを読み取って上限を強制します。利用イベントは冪等であるため、リトライによって二重請求が生じることはありません。
テナント別オブザーバビリティ
すべてのログ行、メトリクス、トレースには、テナント識別子がタグとして付与されます。これにより、サポートは特定の顧客の体験を確認でき、エンジニアはほかのテナントが気づく前にノイジーネイバーを検知できます。セキュリティに関わる操作の監査ログは、各テナントの管理者が利用できます。
トレードオフ
設計判断
| 判断 | 理由 |
|---|---|
| 行レベルセキュリティを備えた共有データベースを既定とする | テナントごとのデータベースは最も強い分離を実現しますが、顧客が増えるたびにコスト、マイグレーション時間、運用作業が積み上がります。行レベルセキュリティを用いたプール方式では、管理対象の数を小さく、アップグレードを単純に保てます。この方式は、アプリケーションコードでフィルターが抜け落ちてもデータ漏えいにならないよう、データベースが強制するポリシーに依拠します。 |
| 専用スキーマや専用データベースは基本構成ではなく、階層の1つとして提供 | 契約上またはデータ所在地の理由から、プール方式では提供できない分離を必要とする顧客もいます。複数の分離モデルをサポートするとルーティング層が加わり、マイグレーションが複雑になるため、要件上それが正当化されるテナントに限定します。 |
| コントロールプレーンをアプリケーションプレーンから分離 | テナント管理をプロダクトのサービスに組み込むほうが、最初は早く構築できます。分離するとデプロイすべきシステムが2つになりますが、プロビジョニング、エンタイトルメント、課金の明確な置き場所が得られます。これらは独自のスケジュールで変更でき、障害が起きても顧客のトラフィックに影響しません。 |
| テナントコンテキストは署名付きトークンからのみ導出 | ヘッダーやリクエストボディからテナント識別子を受け取る方法は便利ですが、テナントをまたぐアクセスの不具合を生む典型的な原因です。検証済みのクレームからのみ取得することで、この種の誤りを排除できます。その代償として、一部の社内ツールやサポート業務のワークフローには、より慎重な設計が求められます。 |
| プラン別のデプロイではなく、実行時にエンタイトルメントを評価 | プランごとに異なるビルドを出荷すると、テストが分断され、リリースが遅くなります。実行時にエンタイトルメントをチェックする単一のコードベースでは、リクエスト経路に参照が1つ加わりますが、これはキャッシュされます。また、リリースなしでプランと上限を変更できます。 |
意図する成果
- 開発者の規律ではなく、プラットフォームによって強制されるテナント分離
- サインアップから利用可能なテナントの用意までが自動化されたオンボーディング
- 顧客ごとの個別開発なしで満たせる、エンタープライズのアイデンティティ要件
- テナント単位で把握できる利用状況と性能
この設計が達成を目指すものです。参考例であり、クライアント案件の実測結果ではありません。
代表的な技術
- TypeScript
- Next.js
- Node.js
- Spring Boot
- PostgreSQL
- Redis
- Kafka
- Kubernetes
- Terraform
- OIDC / SAML / SCIM
- OpenTelemetry
