日本語
  • saas
  • multi-tenancy
  • postgres
  • row-level security
  • rls
  • trigger function
  • multi-tenant architecture
  • single-tenant architecture

PostgreSQL を使用したマルチテナントの実装:シンプルな実例で学ぶ

PostgreSQL の行レベルセキュリティ (RLS) とデータベースロールを使用して、テナント間のデータの安全な分離を実現するためのマルチテナントアーキテクチャを実装する方法を、実例を通じて学びます。

Yijun
Yijun
Developer

以前の記事では、マルチテナントの概念とその製品および実世界のビジネスシナリオにおける適用について詳しく説明しました。

この記事では、技術的な観点から PostgreSQL を使用してアプリケーションのためのマルチテナントアーキテクチャを実装する方法を探ります。

シングルテナントアーキテクチャとは?

シングルテナントアーキテクチャとは、各顧客がアプリケーションとデータベースの専用インスタンスを持つソフトウェアアーキテクチャを指します。

このアーキテクチャでは、各テナントのデータとリソースは他のテナントから完全に分離されています。

シングルテナント

マルチテナントアーキテクチャとは?

マルチテナントアーキテクチャは、複数の顧客(テナント)が同じアプリケーションインスタンスとインフラストラクチャを共有しつつ、データの分離を維持するソフトウェアアーキテクチャです。このアーキテクチャでは、単一のソフトウェアインスタンスが複数のテナントにサービスを提供し、各テナントのデータはさまざまな分離メカニズムを通じて他と分離されています。

マルチテナント

シングルテナントアーキテクチャとマルチテナントアーキテクチャの違い

シングルテナントアーキテクチャとマルチテナントアーキテクチャは、データの分離、リソース利用、スケーラビリティ、管理と保守、セキュリティなどの点で異なります。

シングルテナントアーキテクチャでは、各顧客が独立したデータスペースを持ち、リソース利用は低いですが、カスタマイズが比較的簡単です。通常、特定の顧客のニーズに合わせたシングルテナントソフトウェア(例:特定のファブリックサプライヤー向けの在庫管理システムや個人的なブログのウェブアプリ)が含まれます。これらに共通するのは、各顧客がアプリケーションサービスの独立したインスタンスを占有し、特定の要求を満たすためのカスタマイズを容易にすることです。

一方、マルチテナントアーキテクチャでは、複数のテナントが同じ基盤リソースを共有するため、リソースの利用が高くなります。ただし、データの分離とセキュリティを確保することが重要です。

マルチテナントアーキテクチャは、サービスプロバイダーが異なる顧客に標準化されたサービスを提供する場合に好まれるソフトウェアアーキテクチャです。これらのサービスは通常、カスタマイズが少なく、すべての顧客が同じアプリケーションインスタンスを共有します。アプリケーションの更新が必要な場合、1つのアプリケーションインスタンスを更新することは、すべての顧客のアプリケーションを更新することと同等です。たとえば、CRM(顧客関係管理)は標準化された要求です。これらのシステムは通常、すべてのテナントに同じサービスを提供するためにマルチテナントアーキテクチャを使用します。

マルチテナントアーキテクチャのテナントデータ分離戦略

マルチテナントアーキテクチャでは、すべてのテナントが同じ基盤リソースを共有するため、テナント間のリソースの分離が重要です。この分離は必ずしも物理的である必要はなく、単にテナント間のリソースが互いに見えないことを保証するだけで済みます。

アーキテクチャ設計において、テナント間のリソース分離のさまざまな度合いを達成することができます:

分離から共有まで

一般に、テナント間でより多くのリソースが共有されるほど、システムのイテレーションと保守のコストは低くなります。逆に、共有されるリソースが少ないほど、コストが高くなります。

実世界の例を用いたマルチテナント実装の開始

この記事では、CRM システムを例として使用して、シンプルで実用的なマルチテナントアーキテクチャを紹介します。

すべてのテナントが同じ標準サービスを使用していることを認識しているため、すべてのテナントが同じ基本的なリソースを共有することにしました。そして、PostgreSQL の 行レベルセキュリティ を使用して、データベースレベルで異なるテナント間のデータを隔離します。

さらに、テナントの権限管理をより良くするために、各テナントに対して個別のデータ接続を作成します。

次に、このマルチテナントアーキテクチャをどのように実装するかを紹介します。

PostgreSQL を使用したマルチテナントアーキテクチャの実装方法

すべてのリソースにテナント識別子を追加

CRM システムでは、多くのリソースがあり、それらは異なるテーブルに保存されます。たとえば、顧客情報は customers テーブルに保存されます。

マルチテナントを実装する前は、これらのリソースはどのテナントにも関連付けられていませんでした:

異なるリソースを持つテナントを区別するために、tenants テーブルを導入してテナント情報を保存します(db_userdb_user_password は各テナントのデータベース接続情報を保存するために使用され、後で詳しく説明します)。さらに、各リソースに tenant_id フィールドを追加して、どのテナントに属しているかを識別します:

これで、各リソースは tenant_id と関連付けられており、理論的には where 句をすべてのクエリに追加して各テナントのリソースへのアクセスを制限できるようになりました:

一見するとこれがシンプルで実行可能に思えるかもしれませんが、以下の問題があります:

  • ほとんどのクエリがこの where 句を含むため、コードが煩雑になり、特に複雑な結合ステートメントを書くときに保守が難しくなります。
  • コードベースの新参者がこの where 句を追加するのを忘れる可能性が高いです。
  • 異なるテナント間でデータが真に隔離されておらず、各テナントが他のテナントのデータにアクセスする権限を持つことになります。

したがって、このアプローチは採用しません。それに代わって、PostgreSQL の行レベルセキュリティを使用してこれらの懸念を解決します。ただし、進む前に、テナントごとにこの共有データベースへの専用のデータベースアカウントを作成します。

テナント用のデータベースロールの設定

データベースに接続できるユーザーごとにデータベースロールを割り当てるのは良いプラクティスです。これにより、各ユーザーのデータベースへのアクセスをより良く制御でき、異なるユーザー間の操作を分離し、システムの安定性とセキュリティを向上させることができます。

すべてのテナントが同じデータベース操作権限を持っているため、これらの権限を管理するために基本ロールを作成できます:

次に、各テナントのロールを区別するために、基本ロールから継承されたロールがテナントの作成時に各テナントに割り当てられます:

次に、テナントごとのデータベース接続情報が tenants テーブルに保存されます:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

このメカニズムにより、各テナントに専用のデータベースロールが提供され、これらのロールは crm_tenant ロールに付与された権限を共有します。

次に、crm_tenant ロールを使用してテナントの権限範囲を定義できます:

  • テナントは、すべての CRM システムリソーステーブルへの CRUD アクセスを持つ必要があります。
  • CRM システムリソースに関連しないテーブルは、テナントには見えないようにするべきです(仮に systems テーブルだけとします)。
  • テナントは tenants テーブルを変更することができず、自分自身のテナント ID をデータベース操作時にクエリするため、iddb_user フィールドのみが見えるべきです。

テナントのロールが設定されると、テナントがサービスへのアクセスを要求するときに、そのテナントを表すデータベースロールを使用してデータベースと対話できます:

PostgreSQL の行レベルセキュリティを使用してテナントデータを保護

ここまでで、テナント用の対応するデータベースロールが確立されましたが、これだけではテナント間のデータアクセスを制限することはできません。次に、PostgreSQL の行レベルセキュリティ機能を活用して、各テナントが自分のデータにのみアクセスできるようにします。

PostgreSQL では、テーブルに 行セキュリティポリシー を設定して、どの行がクエリによってアクセスされるか、またはデータ操作コマンドによって変更されるかを制御できます。この機能は RLS(行レベルセキュリティ)とも呼ばれます。

デフォルトでは、テーブルに行セキュリティポリシーはありません。RLS を利用するには、テーブルに対して RLS を有効にし、テーブルにアクセスするたびに実行されるセキュリティポリシーを作成する必要があります。

CRM システムの customers テーブルを例に取り、そのテーブルで RLS を有効にし、各テナントが自分の顧客データにのみアクセスできるようにするセキュリティポリシーを作成します:

セキュリティポリシーを作成するステートメントでは:

  • for all (オプション) は、このアクセスポリシーがテーブルに対してselect, insert, update, および delete 操作に使用されることを示します。特定の操作のためにアクセスポリシーを指定するには、for に続けてコマンドキーワードを使用できます。
  • to crm_tenant は、このポリシーがデータベースロール crm_tenant を持つユーザー、つまりすべてのテナントに適用されることを示します。
  • as restrictive はポリシーの実施モードを指定し、アクセスが厳密に制限されるべきことを示します。デフォルトでは、テーブルには複数のポリシーがあり、複数の permissive ポリシーは OR 関係で結合されます。このシナリオでは、CRM システムテナントに所属するユーザーに対してこのポリシーチェックを必須にしたいので、このポリシーを restrictive として宣言します。
  • using 式は、特定のアクセス条件を定義し、現在のクエリデータベースユーザーが自分のテナントに所属するデータのみを見ることができるように制限します。この制約は、一部のコマンド(select, update, または delete)によって選択された行に適用されます。
  • with check 式は、データ行を変更するときに必要な制約(insert または update)を定義し、テナントが自分自身のレコードを追加または更新することのみを保証します。

RLS を使用してリソーステーブルへのテナントアクセスを制約することは、次のような利点を提供します:

  • このポリシーは、すべてのクエリ操作(select, update, または delete)に where tenant_id = (select id from tenants where db_user = current_user) を効果的に追加します。 たとえば、select * from customers を実行すると、これは select * from customers where tenant_id = (select id from tenants where db_user = current_user) を実行することに等しいです。これにより、アプリケーションコードに where 条件を明示的に追加する必要がなくなり、コードが単純化され、エラーの可能性が減少します。
  • 異なるテナント間のデータアクセス権限をデータベースレベルで集中管理し、アプリケーションの脆弱性や不整合のリスクを軽減し、システムのセキュリティを向上させます。

ただし、注意すべき点がいくつかあります:

  • RLS ポリシーは、データの各行に対して実行されます。RLS ポリシー内のクエリ条件が複雑すぎると、システムのパフォーマンスに大きな影響を与える可能性があります。幸いなことに、私たちのテナントデータチェッククエリは十分に単純で、パフォーマンスに影響を与えることはありません。後で RLS を使用して他の機能を実装する予定がある場合は、RLS のパフォーマンスを最適化するために、Supabase の 行レベルセキュリティのパフォーマンス推奨事項 を参照することができます。
  • RLS ポリシーは、insert 操作時に tenant_id を自動的に埋め込むことはありません。それらはテナントが自分自身のデータを挿入することのみを制約します。つまり、データを挿入する際には、依然としてテナント ID を提供する必要がありますが、これはクエリプロセスと一貫性がなく、開発中に混乱を引き起こしやすく、エラーの可能性を高めます(これは次のステップで対処されます)。

customers テーブルに加えて、すべての CRM システムリソーステーブルに同じ操作を適用する必要があります(このプロセスは少し面倒かもしれませんが、初期化中に設定自動化プログラムを書いて対応できます)、これにより異なるテナントのデータが隔離されます。

データ挿入用のトリガーファンクションの作成

前述のように、RLS (行レベルセキュリティ)により、クエリ実行時に tenant_id の存在を心配する必要がなくなりますが、insert 操作の場合には依然として対応する tenant_id を手動で指定する必要があります。

データ挿入に対する RLS と同様の便利さを達成するために、データ挿入時にデータベースが tenant_id を自動的に処理できるようにする必要があります。

これには明確な利点があります:アプリケーション開発のレベルでは、データがどのテナントに属しているかを考慮する必要がなくなり、エラーの可能性が減少し、マルチテナントアプリケーションを開発する際に心配する負担が軽減されます。

幸いなことに、PostgreSQL は強力なトリガー機能を提供しています。

トリガーはテーブルに関連付けられた特別な関数であり、テーブル上で特定の操作(挿入、更新、削除など)が実行されると自動的に特定のアクションを実行します。これらのアクションは、行ごとに(行レベル)またはステートメント全体に(ステートメントレベル)トリガーされることがあります。トリガーを使用すると、特定のデータベース操作の前後にカスタムロジックを実行でき、私たちの目標を簡単に達成することができます。

まず、set_tenant_id トリガーファンクションを作成し、各データ挿入の前に実行します:

次に、このトリガーファンクションを customers テーブルに挿入操作用に関連付けます(テーブルに対して RLS を有効にするのと同様に、このトリガーファンクションもすべての関連テーブルに関連付ける必要があります):

このトリガーにより、挿入されたデータに正しい tenant_id を含めることが保証されます。新しいデータにすでに tenant_id が含まれている場合、トリガーファンクションは何もしません。それ以外の場合、現在のユーザー情報に基づいて tenants テーブルから tenant_id フィールドを自動的に埋め込みます。

これにより、テナントによるデータ挿入時、データベースレベルで tenant_id を自動的に処理することが達成され、アプリケーションレベルで tenant_id を記入する必要がないようになります。

まとめ

この記事では、CRM システムを例に取り上げ、PostgreSQL データベースを活用した実用的な解決策を示し、マルチテナントアーキテクチャの実践的応用について詳しく探りました。

データベースロール管理、アクセス制御、そして PostgreSQL の行レベルセキュリティ機能について説明し、テナント間のデータ分離を確保しました。さらに、トリガーファンクションを利用して、異なるテナントを管理するデベロッパーの認知負担を軽減しました。

この記事は以上です。マルチテナントアプリケーションをユーザーアクセス管理でさらに強化したい場合、マルチテナントアプリを構築するための Logto 組織の簡単なガイド を参照して、さらなる洞察を得ることができます。