オライリー出版のマルチテナントSaaSアーキテクチャの構築を読んで、大事なポイントや気づいた点を個人的にまとめたいと思う。
とはいえ、全体で17章もあるボリュームの書籍であるため、このブログでも何回かに分けて取り扱いたい。(最終的な総括も記事にする予定)
前回の記事はこちら。
あくまで読者メモなのでかなり内容を間引いている。
より深く情報を得たい人は実際に書籍を手にとって読んで欲しい。
6章 テナントの認証とルーティング
- 導入
- マルチテナントの”正面玄関”について考える。
- ここでユーザー認証を行う。
- 認証モデルへの影響。
- ログインページの公開について確認する。
- 最後に、アプリケーションにJWTがどのように組み込まれて、認証のコンテキストがどのようにルーティングされるか。
- マルチテナントの”正面玄関”について考える。
- 正面玄関から入る。
- 正面玄関とは単にURLを指すものではない。
- テナントがシステムにアクセスする方法は複数の選択肢がある。
- システムのドメインにテナント名を含めて、テナントの割り当てとルーティング戦略の一環としてこのドメインを利用する。
- テナント独自のドメインを持たせる。
- 全てのテナントに1つのドメインを使用する。
- テナントのコンテキストをシステムに注入するために、環境の認証フローが必要。
- テナントドメイン経由でのアクセス
- ドメイン駆動型のアクセスモデル:テナントを識別するための情報が含まれるドメイン経由でアクセスする。
- オンボーディグプロセス中にテナントのドメインが作成される。
- テナントマッピング:異なるドメインからの内向きのテナントリクエスト。
- これが適用される一般的な用途が少なくとも2つある。
- 認証:システムがテナントユーザーを認証するとき、受信した認証リクエストを対応するアイデンティティ構造に割り当てる必要がある。
- 単一の共有アイデンティティプロバイダーがないテナント環境に適用される。
- アプリケーションのリクエストルーティング:1つまたは複数のサイロ化されたテナントリソースを使用している環境では、受信したテナントリクエストを特定のリソースにそれぞれルーティングする必要がある。
- マッピングサービスはテナントコンテキストを使用して、特定のリクエストに対して経路を識別する。
- 認証:システムがテナントユーザーを認証するとき、受信した認証リクエストを対応するアイデンティティ構造に割り当てる必要がある。
- これが適用される一般的な用途が少なくとも2つある。
- テナントごとのサブドメインモデル
- tenant1.abc-software.comやtenant2.abc-software.comのようなドメイン。(abc-softwareはSaaS企業のこと)
- メリット:テナントごとに固有のドメインを作成する複雑性やオーバーヘッドを伴わず、テナントごとに独自のアクセス先を提供できる。
- テナントごとのバニティドメインモデル
- テナントが顧客にブランド体験を提供するシナリオ。
- サブドメインモデルと似ていて、オリジンを使用してテナント名を抽出し、後続の工程で使用するためにテナントコンテキストに割り当てる。
- テナントドメインを使用したオンボーディング
- ドメインを使用してテナントを識別する場合は、オンボーディングモデルにどのように組み込むか考慮が必要。
- サブドメインモデルはオンボーディングフローへの影響は小さい。
- バニティドメインモデルの場合は、そのドメインをマルチテナント環境に利用できるようにサポートが必要。
- ドメインの移行も考慮する場合はさらに複雑化する。
- ドメイン駆動型のアクセスモデル:テナントを識別するための情報が含まれるドメイン経由でアクセスする。
- 単一ドメイン経由でのアクセス
- B2Cモデルを採用している場合、すべてのテナントに対して1つのドメインを使用する。
- 共有ドメイン(www.saasco.com)からコントロールプレーン内のアイデンティティプロバイダーに転送する。
- 共通のアイデンティティ構造
- 特に問題はない。
- すべてのユーザーを1つのアイデンティティプロバイダー構造にまとめるということは、システムの各ユーザーは1つのテナントのみに割り当てられることになる。
- テナントごとに異なるアイデンティティ構造
- ユーザーの認証にどのアイデンティティ構造を使用すべきか判断するコンテキストがない。
- 解決策1:ユーザーのメールアドレスのドメインを使用して、ユーザーを特定のテナントに割り当てる。
- 解決策2:ここのユーザー識別子を特定のテナントに割り当てる。テナント管理DBを利用する。
- ユーザーの認証にどのアイデンティティ構造を使用すべきか判断するコンテキストがない。
- 間接層の課題
- 一般的な認証プロセスで、中間システムを介在することで想定通りの動作をしない可能性がある。
- 共通のアイデンティティ構造
- マルチテナントの認証フロー
- 認証フローの例
- 1. tenan1.saasco.comにアクセス
- 2と3. テナント管理サービスにアクセスして、アイデンティティプロバイダー構造を決定
- 4. アイデンティティプロバイダーを呼び出して認証情報を渡す。
- 5と6. JWTを交換する。
- 7. テナントコンテキストをふくむJWTを取得したので、このトークンをマイクロサービスの呼び出しに埋め込む。
- フェデレーション認証
- アイデンティティの情報が広範囲に分散される状況を検討し始めたときに複雑化する。
- サードパーティのプロトコルをサポートできるかがポイント。
- サードパーティにテナントコンテキストを提供するカスタムクレームを含めるようにリクエストすることはできない。
- マルチテナントの設計は、テナントコンテキストを含むJWTを発行する機能に依存する。
- Amazon Cognitoを使用すれば、サードパーティのプロバイダーから認証されるユーザー向けに、Cognito内のカスタムクレームを設定できる。
- 認証から返されるJWTにカスタムクレームを埋め込める。
- これにより、カスタムクレームを管理しながら、サーボパーティプロバイダーをサポートできる。
- 認証フローの例
- 認証済みテナントのルーティング
- 認証プロセスから取得されるコンテキストは、アプリケーションの以降のルーティングに直接影響する。
- 場合によっては、ルーティング戦略が選択する認証モデルにも影響を及ぼす可能性がある。
- テナントのデプロイモデルがプール・サイロで混在する場合、マルチテナントアーキテクチャにルーティングの概念を実装して、このルーティングをどのように実現するか決定しなければならない。
- アプリケーションのルーティングモデルは、SaaSソリューションのオンボーディング体験にも影響する。
- 新しいテナントがシステムに登録されるたびに、ルーティングインフラストラクチャの構成を更新して、新しいテナントのワークロードをルーティングするために必要な構造をプロビジョニングする必要がある。
- 認証プロセスから取得されるコンテキストは、アプリケーションの以降のルーティングに直接影響する。
- 様々な技術スタックによるルーティング 2つの例を紹介
- サーバーレスのテナントルーティング(Lambda, API Gateway)
- API Gatewayは全てのアクティビティがアプリケーションに流れる重要な経路とみなすことができる。
- S3で公開されているWebアプリケーション。このアプリケーションは、API Gatewayを介してリクエストを行い、様々なマイクローサービスの機能として構成されrたLambda関数にルーティングする。
- 全てプール化されている場合、ゲートウェイは全てのリクエストをテナントのコンテキストに関係なく、対象となる関数に単純に送信される。
- マイクロサービスが、一部または全てがサイロモデルで構成されている場合、ゲートウェイはテナントコンテキストを検証し、リクエストを正しいテナント関数にルーティングする必要があります。
- 関連付けを実現する方法1:テナントがサブドメインを使用してシステムにアクセスする場合、HTTPリクエストヘッダーのオリジンを使用してテナントを特定のゲートウェイのURLに割り当てることができる。
- 関連付けを実現する方法2:全てのテナントに対して1つのドメインを使用しており、各テナントのAPI Gateway URLを紹介するためにテナント管理サービスを使用する。
- リクエストのたびに関連付けを処理をすると、オーバーヘッドやレイテンシーの問題が発生する。
- これをクライアント側で解決するのは違和感。
- 解決策として、直近で割り当てられたテナントを保持するために、マッピングまたはゲートウェおレベルのキャッシュ戦略を導入する。
- リクエストのたびに関連付けを処理をすると、オーバーヘッドやレイテンシーの問題が発生する。
- ここでも拡張性を意識する。テナントごとにゲートウェイを用意すると、テナントの数が多い環境では拡張性が損なわれる。
- コンテナのテナントルーティング
- k8sのサービスメッシュを使用したルーティング体験。ルーティングモデルの実装にIstioを使用した例。
- フロー
- 1. テナントごとのサブドメインモデルを使用して正面玄関から入る。
- 2. ネットワークロードバランサーを介してリクエストを送信する。
- 3. k8sクラスター内の独自のNamespaceで稼働するIstio Ingressゲートウェイに到達する。
- このゲートウェイは、アイデンティティプロバイダーやアプリケーションのマイクロサービスへのルーティングに必要な全てのポリシーを管理し、適用する。
- 4. (テナントが認証されていないと仮定すると)ゲートウェイはリクエストをEnvoyリバースプロキシを介して送信する。
- リバースプロキシは、送信元のサブドメインを使用して、別のk8s Namespaceでそれぞれ稼働しているテナント固有のOIDCプロキシに認証リクエストをルーティングする。
- OIDCプロキシは、それぞれに対応するテナントのアイデンティティ構造に認証リクエストを転送する。
- 5. テナントユーザーが認証されると、ゲートウェイはリクエストをアプリケーションのサービスに送信する。
- ゲートウェイはリクエストの送信元を検証し、各リクエスストを適切なテナントのNamespaceにルーティンする必要がある。
- サーバーレスのテナントルーティング(Lambda, API Gateway)
7章 マルチテナントサービスの構築
- 導入
- これまでコントロールプレーンを掘り下げてテナントの概念を導入するためのサービス群について紹介してきた。
- テナントのオンボーディング方法、アイデンティティの確立方法、認証方法、アプリケーションのサービスにテナントコンテキストを注入する方法。
- ここからは、アプリケーションプレーンに注目する。
- この章では、マルチテナントのワークロードの特性が、サービスの設計と分割のアプローチ方法にどのように影響するのかをみる。
- 例えば、分離、ノイジーネイバー、データパーティショニング。サービスの規模、デプロイ、影響範囲への新しいアプローチ。
- これまでコントロールプレーンを掘り下げてテナントの概念を導入するためのサービス群について紹介してきた。
- マルチテナントサービスの設計(サービスの規模、構成、一般的な分割戦略)
- 従来のソフトウェア環境におけるサービス
- サービス全体が完全に個々の顧客専用になっている。
- 単一の顧客が要求する拡張性、パフォーマンス、および耐障害性の要件を満たすことが重要。
- プール型マルチテナント環境におけるサービス
- テナントがシステムにかける負荷には大きなばらつきがある。
- 環境には新しいテナントがいつでも登録される可能性がある。
- サービスは、各テナントのペルソナの拡張性、パフォーマンス、影響範囲の要件を全てなんらかの形で予測する必要がある。
- あるテナントが別のテナントの体験に影響を与えるノイジーネイバー問題を起こさないようにする必要がある。
- 共有インフラストラクチャの利点が、絶えず変化する顧客の利用状況をサポートしなければならない現実と相反する。
- 過剰なプロビジョニングはSaaSビジネスモデルの効率性と規模の経済性の目標と正反対。
- サービス設計のアプローチは、テナントが環境に課す様々な動的要因に対処するための選択肢を増やすことに重点を置く。
- 既存のベストプラクティスの拡張
- マルチテナントサービスは一般的に使用される方法論や戦略の多くをそのまま反映できる。
- ドメイン駆動設計、ビジネスサブドメイン、イベントストリーミング。
- ノイジーネイバーへの対応
- マルチテナントと共有インフラストラクチャの特性上、ノイジーネイバー問題がより重要になる。
- サービスが肝要なプロビジョニングや他のテナントに影響を与えることなく、多数のペルソナやワークロードに対応できるほどの拡張性を持っているなら問題ない。
- 水平方向の拡張だけではマルチテナント環境へは対処できない。
- 特定のAPIエントリーポイントが重い処理を実施してパフォーマンスに影響を与える。
- サービスの責任範囲については別の考え方をして、マルチテナント環境の様々な負荷に対応するためにどう拡張するかを検討する。
- あるサービスをさらに小さなサービスに分割することで、より対象を絞った拡張性の選択肢を作る。
- 商品サービスはサムネイルサービスに分割し、注文サービスはスタンドアロンの税務サービスに分割する。
- マルチテナントでは、テナントのペルソナやワークロードの多様性を考慮し、SaaSアーキテクトはこの課題を解決するために取り組む。
- 拡張効率の悪さがボトルネックになり、過剰なプロビジョニングされたサービスが顕著に増える傾向にある。
- 一般的なアプローチも有効だけど、マルチテナント環境向けのサービス設計はもっと注意する必要がある。
- ノイジーネイバーの問題は日々変化する。監視してどのように変化するのかを予測する。
- サイロ化するサービスの特定
- サービスがサイロ型の体験で、いつ、必要に応じてデプロイされるかは、実は強い相関関係がある。
- サイロ化の背景は、特定のシステムやテナントの要件をサポートするためであることが多い。
- リソースのサイロ化は、運用、コスト、デプロイ、管理の複雑さに影響する。そのため、サイロにデプロイするサービスの数を制限するのが理想的。
- 顧客のサービス分離要件に対応するには、やはりサービスを小さなサービスに分割しておくのが大事。
- 大規模なサービスを分割して、何がサイロ化され、何がサイロ化されないかをより細かく制御する。
- サイロ化する必要性に基づいて、サービスをグループ化するだけで済むかもしれない。
- サイロとプールの混在をサポートする必要があるサービスを設計する場合、プールモデルにできる限り多くのサービスを配置できるように、サービスをどのように分割できるかを考えるべき。
- プールモデルではテナントの要望に応えられない場合もある。実際の運用状況に基づいて機能の一部を切り出して、サイロモデルでデプロイすることを選択する。
- 著者はただ再露にサービスを移行することは非推奨。稼働中のシステムから運用上の洞察を収集するまで、境界線を見つけられない。
- コンピューティング技術の影響(コンテナ・サーバーレス)
- いずれの構成も、特定の操作(createOrderメソッド)が大半のリクエストを処理していると仮定する。
- コンテナベースの場合、環境全体の拡張効率を向上させるために、このサービスをどのようにリファクタリングするか考える。
- コンテナでは、機能が全てパッケージ化され、まとめて拡張、デプロイされるため、コンテナが拡張の単位になる。
- サーバーレスモデルの場合、サービスの一部としてデプロイされる各操作は、独立してデプロイ、管理、拡張できる個別の関数を表しています。
- createOrderメソッドに過度な負荷がかかると、関数が自然と拡張する。
- 使用するコンピューティングモデルがサービスの分割モデルに影響を与える。
- ストレージに関する考慮事項の影響
- マルチテナントのデータは、各テナントが独自の専用ストレージ構造を持つサイロモデルに保存することも、共有ストレージ構造内でテナントのデータが混在するプールモデルに保存することも可能。
- 顧客が共有ストレージのリソースを取り扱う場合、データに対する様々な操作をどのように効果的に拡張できるか考慮する。
- 例えば、全てのテナントのデータを共有テーブルに格納するRDBは、何千ものテナントがクエリを発行する。アンチパターン。
- ストレージに合わせてサービスの粒度を荒くも細かくもする必要がある。
- ノイジーネイバー、コンプライアンス、ティアリング、分離を考慮する。
- メトリクスを用いた設計の分析
- 基本的な監視データでは、個々のテナントやティアの利用状況とアクティビティのパターンを評価できない。
- メトリクスによって、これまで検討してきた様々な要因(ノイジーネイバー、ティアリング、パフォーマンス)などを評価できる。
- メトリクスと分析の構築には本気で投資する必要がある。データがなければ、テナントのワークロードとアクティビティの変化がアーキテクチャに与える影響を調べられない。
- 従来のソフトウェア環境におけるサービス
- マルチテナントサービスの内部
- 導入
- 開発者がマルチテナント意識することがないように最大限の努力をすべき。
- 一般的なコードと同じようにわかりやすく、親しみやすいものにすることが目標。余計な手間や負担はかけない。
- テナントコンテキストの抽出
- ここまで紹介したように、JWTに埋め込まれたコンテキストを活用する。
- JWTはサービスに送信される各HTTPリクエストのヘッダーとして埋め込まれ、「ベアラートークン」として渡される。
- ベアラートークンに関連づけられたテナントに変わって、システムが操作を実行することを許可しているということ。
- トークンからの取り出しには、HTTPリクエスト全体から認可ヘッダーを取り出し、auth_headerにBearer<JWT>を設定する。
- 文字列操作をし、JWTライブラリを使用してデコードし、最終的にカスタムクレームからテナントIDを取得する。
- ソリューションに応じて、他のクレーム(役割、ティアなど)にもアクセスするかもしれない。
- サービス内で各トークンをデコードしなくても、例えばサービスの前段にAPIゲートウェイを置き、そこで処理を担うことも可能。
- 後続処理に必要なテナントコンテキストにアクセスできる。
- テナントコンテキストを用いたログとメトリクス
- 通常、ログに何も手を加えていない場合、特定のテナントと相関関係のない様々な洞察が混在する。
- コンテキストをログメッセージに含めることで、運用チームがここのテナントやティアなどの観点からログが分析できるようになる。
- 単にログメッセージの先頭にテナントコンテキストを追加するだけで、ここのテナントがシステムとどのようにやりとりするかを把握できるようになる。
- 同じログのマインドセットを、メトリクス測定機能にも適用する必要がある。
- テナントコンテキストを用いたデータへのアクセス
- 通常、手を加えなければ、サービスは注文をリクエストしたすべてのテナントに対して同じデータを返すことになってしまう。
- 注文の表示を、呼び出し元のテナントに関連するものに限定する必要がある。
- 自然で簡単な方法は、検索に含まれるパラメーターにテナントを追加すること。
- テナント識別子はすでに用意されているので、それを使ってデータにアクセスする方法を決定するだけ。
- データがプール化されている場合は、注文テーブルにTenantIdキーを追加するだけで、各注文と特定のテナントを関連づけることができる。
- テナント識別子がテーブルのキーになる。
- つまり、これまで使用していたステータスが、指定されたステータスと一致するテナントの全ての注文を返す二次検索パラメータになる。
- サービスがサポートするストレージ戦略に様々な組み合わせがある場合、複雑化する。
- 例えば、システムがティアごとに異なるストレージを提供した場合、各テナントのティアを調べてどのテーブルを使ってリクエストを処理するのかを決定するロジックが必要。
- テナントのティアに基づいて使用されるテーブル名を解決するために、クエリにマッピング処理を追加する必要がある。
- 通常、手を加えなければ、サービスは注文をリクエストしたすべてのテナントに対して同じデータを返すことになってしまう。
- テナント分離のサポート
- マルチテナント環境では、テナントの分離がテナントの信頼に不可欠であるため、テナントごとにデータアクセスを制限するだけでは十分ではない。
- データがどよのうに保存され、アクセスされるかは「データパーティショニング」戦略。
- テナント間のアクセスからリソース(データを含む)を保護する方法を「テナント分離」と呼ぶ。
- テナントリソースの分離とは、開発者が意図的または非意図的にテナントの境界線を超えないようにするために、サービス内のコードを保護するための対策。
- 例えば、クエリに含まれるテナントのパラメーターに関係なく、そのクエリを対象とするテナント分離のポリシーは、そのコードが別のテナントのリソースにアクセスされることを防ぐ。
- 目標は、コードがリソースにアクセスする前に、何らかの方法で分離のコンテキストを取得し、それを使用してリソースアクセスを制限すること。
- 例えば、スコープの適用をAWS STSを使用する。
- この資格情報の値でセッションを宣言して接続する。ポリシーによってアクセスできる範囲が制限される。
- 導入
- マルチテナントの詳細の隠蔽と一元化
- 複雑にすることなくマルチテナントの要素を取り入れたいが、例では簡単に紹介したが実際のところは難しい。
- そこで、これまで取り上げてきた多くを隠蔽できるライブラリに移行する方法を探す。
- 例えば、JWTからテナントコンテキストを取得するために追加したコードを別の関数に移し替える。
- get_tenant_context()関数とする。
- HTTPリクエストを受け取り、JWTを抽出しデコードし、テナントコンテキストを含むカスタムクレームを取得し、JSON形式で返す。
- このライブラリを1度呼び出すだけで良くなり、JWTのポリシー変更も簡単に拡散できる。
- 同様に、ログ、メトリクス、データアクセス、テナント分離のコードにも適用できる。
- ログやメトリクスは、記録するたびにテナントコンテキストを注入する余分なオーバーヘッドを取り除く。
- データアクセスは汎用性が低い。従来のデータアクセスライブラリ(DAL)を使用して、サービスのストレージとのやりとりを抽象化する。
- 傍受ツールと戦略
- 基本的な考え方は、特定の技術構造に組み込まれている機能を活用して、マルチテナントの水平方向の要件をサポートし、サービス簿ビルダーの協力を最小限に抑えながらマルチテナントの運用とポリシーを導入および構成できるかを検討することです。
- アスペクト
- アスペクトは通常、言語またはフレームワークの構成要素として導入される。
- テナントコンテキスト -> 前処理 -> サービス -> 後処理 のフロー。
- アスペクト思考プログラミングでは、テナントからのリクエストの入力と終了時に実行される追加の処理ロジックをサービスに組み込むことができる。
- サイドカー(プロキシ)
- k8sで構築されている場合、サイドカーを使用してマルチテナント戦略をサービスに適用できるか検討すべき。
- サイドカーの利点は、サービスの完全に外側にあること。よって、サービスの連携を必要としない方法で全体にマルチテナントポリシーを適用できる。
- サイドカーはサービスの度と側でテナントコンテキストを傍受して参照し、コンテキストを抽出して、サービスとそれが利用しているリソースに関連するポリシーを適用することができる。
- サイドカーを個別にデプロイして構成することで、より堅牢なマルチテナントの実装方法を確立できるため、サービスと他のリソースとのやりとりをより詳細に制御できるようになる。
- ミドルウェア
- 受信リクエストと対象となる操作sの間に位置するコードを組み込むことができる。
- Node.jsのExpressフレームワークでよく使用され、ここで取り上げたマルチテナントサービス戦略の多くを実装するために必要な組み込み構造をすべて定常している。
- AWS Lambdaレイヤー/Extensions
- AWS LambdaレイヤーまたはExtensionsを使用して共有ライブラリをスタンドアロンの仕組みに移行することができる。
- Lambdaレイヤーでは、基本的にすべてのヘルパーを共有ライブラリに移し、それを独立してデプロイすることができる。
- Lambda関数はこの共有ライブラリのコードを参照するため、そのコードを各サービスの一部にすることなく、さまざまなヘルパー関数にアクセスできる。
- Lambda Extensionは、アスペクトパターンに近いもので、カスタムコードをLambda関数のライフサイクルに関連づけをする。
(この記事ではここまで)