PostgreSQL を使用したマルチテナントの実装:シンプルな実例で学ぶ
PostgreSQL の行レベルセキュリティ (RLS) とデータベースロールを使用して、テナント間のデータの安全な分離を実現するためのマルチテナントアーキテクチャを実装する方法を、実例を通じて学びます。
以前の記事では、マルチテナントの概念とその製品および実世界のビジネスシナリオにおける適用について詳しく説明しました。
この記事では、技術的な観点から PostgreSQL を使用してアプリケーションのためのマルチテナントアーキテクチャを実装する方法を探ります。
シングルテナントアーキテクチャとは?
シングルテナントアーキテクチャとは、各顧客がアプリケーションとデータベースの専用インスタンスを持つソフトウェアアーキテクチャを指します。
このアーキテクチャでは、各テナントのデータとリソースは他のテナントから完全に分離されています。
マルチテナントアーキテクチャとは?
マルチテナントアーキテクチャは、複数の顧客(テナント)が同じアプリケーションインスタンスとインフラストラクチャを共有しつつ、データの分離を維持するソフトウェアアーキテクチャです。このアーキテクチャでは、単一のソフトウェアインスタンスが複数のテナントにサービスを提供し、各テナントのデータはさまざまな分離メカニズムを通じて他と分離されています。
シングルテナントアーキテクチャとマルチテナントアーキテクチャの違い
シングルテナントアーキテクチャとマルチテナントアーキテクチャは、データの分離、リソース利用、スケーラビリティ、管理と保守、セキュリティなどの点で異なります。
シングルテナントアーキテクチャでは、各顧客が独立したデータスペースを持ち、リソース利用は低いですが、カスタマイズが比較的簡単です。通常、特定の顧客のニーズに合わせたシングルテナントソフトウェア(例:特定のファブリックサプライヤー向けの在庫管理システムや個人的なブログのウェブ アプリ)が含まれます。これらに共通するのは、各顧客がアプリケーションサービスの独立したインスタンスを占有し、特定の要求を満たすためのカスタマイズを容易にすることです。
一方、マルチテナントアーキテクチャでは、複数のテナントが同じ基盤リソースを共有するため、リソースの利用が高くなります。ただし、データの分離とセキュリティを確保することが重要です。
マルチテナントアーキテクチャは、サービスプロバイダーが異なる顧客に標準化されたサービスを提供する場合に好まれるソフトウェアアーキテクチャです。これらのサービスは通常、カスタマイズが少なく、すべての顧客が同じアプリケーションインスタンスを共有します。アプリケーションの更新が必要な場合、1つのアプリケーションインスタンスを更新することは、すべての顧客のアプリケーションを更新することと同等です。たとえば、CRM(顧客関係管理)は標準化された要求です。これらのシステムは通常、すべてのテナントに同じサービスを提供するためにマルチテナントアーキテクチャを使用します。
マルチテナントアーキテクチャのテナントデータ分離戦略
マルチテナントアーキテクチャでは、すべてのテナントが同じ基盤リソースを共有するため、テナント間のリソースの分離が重要です。この分離は必ずしも物理的である必要はなく、単にテナント間のリソースが互いに見えないことを保証するだけで済みます。
ア ーキテクチャ設計において、テナント間のリソース分離のさまざまな度合いを達成することができます:
一般に、テナント間でより多くのリソースが共有されるほど、システムのイテレーションと保守のコストは低くなります。逆に、共有されるリソースが少ないほど、コストが高くなります。
実世界の例を用いたマルチテナント実装の開始
この記事では、CRM システムを例として使用して、シンプルで実用的なマルチテナントアーキテクチャを紹介します。
すべてのテナントが同じ標準サービスを使用していることを認識しているため、すべてのテナントが同じ基本的なリソースを共有することにしました。そして、PostgreSQL の 行レベルセキュリティ を使用して、データベースレベルで異なるテナント間のデータを隔離します。
さらに、テナントの権限管理をより良くするために、各テナントに対して個別のデータ接続を作成します。
次に、このマルチテナントアーキテクチャをどのように実装するかを紹介します。
PostgreSQL を使用したマルチテナントアーキテクチャの実装方法
すべてのリソースにテナント識別子を追加
CRM システムでは、多くのリソースがあり、それらは異なるテーブルに保存されます。たとえば、顧客情報は customers
テーブルに保存されます。
マルチテナントを実装する前は、これらのリソースはどのテナントにも関連付けられていませんでした:
異なるリソースを持つテナントを区別するために、tenants
テーブルを導入してテナント情報を保存します(db_user
と db_user_password
は各テナントのデータベース接続情報を保存するために使用され、後で詳しく説明します)。さらに、各リソースに tenant_id
フィールドを追加して、どのテナントに属しているかを識別します:
これで、各リソースは tenant_id
と関連付けられており、理論的には where
句をすべてのクエリに追加して各テナントのリソースへのアクセスを制限できるようになりました:
一見するとこれがシンプルで実行可能に思えるかもしれませんが、以下の問題があります:
- ほとんどのクエリがこの
where
句を含むため、コードが煩雑になり、特に複雑な結合ステートメントを書くときに保守が難しくなります。 - コードベースの新参者がこの
where
句を追加するのを忘れる可能性が高いです。 - 異なるテナント間でデータが真に隔離されておらず、各テナントが他のテナントのデータにアクセスする権限を持つことになります。
したがって、このアプローチは採用しません。それに代わって、PostgreSQL の行レベルセキュリティを使用してこれらの懸念を解決します。ただし、進む前に、テナントごとにこの共有データベースへの専用のデータベースアカウントを作成します。
テナント用のデータベースロールの設定
データベースに接続できるユーザーごとにデータベースロールを割り当てるのは良いプラクティスです。これにより、各ユーザーのデータベースへのアクセスをより良く制御でき、異なるユーザー間の操作を分離し、システムの安定性とセキュリティを向上させることができます。
すべてのテナントが同じデータベース操作権限を持っているため、これらの権限を管理するために基本ロールを作成できます:
次に、各テナントのロールを区別するために、基本ロールから継承されたロールがテナントの作成時に各テナントに割り当てられます:
次に、テナントごとのデータベース接続情報が tenants
テーブルに保存されます:
id | db_user | db_user_password |
---|---|---|
x2euic | crm_tenant_x2euic | pa55w0rd |
このメカニズムにより、各テナントに専用のデータベースロールが提供され、これらのロールは crm_tenant
ロールに付与された権限を共有します。
次に、crm_tenant
ロールを使用してテナントの権限範囲を定義できます:
- テナントは、すべての CRM システムリソーステーブルへの CRUD アクセスを持つ必要があります。
- CRM システムリソースに関連しないテーブルは、テナントには見えないようにするべきです(仮に
systems
テーブルだけとします)。 - テナントは
tenants
テーブルを変更することができず、自分自身のテナント ID をデータベース操作時にクエリするため、id
とdb_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 組織の簡単なガイド を参照して、さらなる洞察を得ることができます。