PostgreSQLで安全なマルチテナントデータベースを設計する:HealthTech事例
B2B SaaSプラットフォーム向けのマルチテナントPostgreSQLデータベースを設計するためのアーキテクチャ解説。Row-Level Security(RLS)を活用したクライアント間の厳格なデータ分離を実現する。
このページの内容
B2B SaaSプラットフォームの構築において、古典的なアーキテクチャ上の課題があります。それは、単一のアプリケーションインスタンスから数百または数千の顧客にサービスを提供しながら、データの完全なる分離をどのように確保するかということです。これはHealthTechの領域において特に重要であり、従業員のウェルネスデータを追跡するプラットフォームは、HIPAAなどの規制に見られる原則に準拠する必要があるためです。
本ケーススタディでは、従業員のフィットネeschallengeとうつ病チェックインを追跡する架空のB2Bウェルネスプラットフォーム「WellnessCorp」のデータベースアーキテクチャを設計します。マルチテナンシーの問題に直接取り組み、効率的なモデルを選択し、PostgreSQLデータベース内で強固なデータ分離を実装します。
PostgreSQLの強力な行レベルセキュリティ(RLS)機能で強化された共有スキーマモデルを使用して、堅牢でスケーラブルかつ安全なマルチテナントデータベーススキーマを構築します。
前提条件:
- SQLとデータベースの基本的な理解
- PostgreSQLの familiarity(RLSにはバージョン9.5以上が必要)
- 例題をテストするための実行中のPostgreSQLインスタンスへのアクセス
問題の理解:マルチテナンシーの三竦み
マルチテナントデータベースを設計する際、一般的に分離、コスト運用コスト、業務の複雑さの間でトレードオフを伴う3つの主要な選択肢に直面します。
- テナントごとにデータベース(サイロ): 最高レベルの分離。各顧客が独自のデータベースを取得します。最大のセキュリティを提供しますが、高価でリソース集約적이며、スケールでの管理と移行が噩梦적입니다。
- テナントごとにスキーマ(ブリッジ): 中間的な解決策。全顧客が単一のデータベースを共有しますが、それぞれが独自のスキーマ(テーブルの論理的グループ分け)を持っています。良好な分離を提供しますが、テナント数が多いと管理が複雑になり、数千のスキーマでPostgreSQLのパフォーマンス問題が発生場合があります。
- 共有スキーマ、共有テーブル(プール): 最もコスト効率が良くスケーラブルなアプローチ。全顧客が同じテーブルを共有し、特定のカラム(例:
organization_id)がどの行がどのテナントに属するかを区別します。主な課題はデータ分離の実施です。アプリ케이션コードのバグ(WHERE organization_id = ?句の忘れなど)が1つのテナントのデータを別のテナントに公開してしまう可能性があります。
WellnessCorpでは、スケーラビリティとコスト効率のために共有スキーマモデルを採用します。データ漏洩問題を解決するために、アプリ케이션コードに依存しません。代わりに、行レベルセキュリティを使用してデータ分離の責任をデータベース自体に委任します。
前提条件
開始する前に、PostgreSQLがインストールされていることを確認してください。メインデータベースと、アプリケーションが使用する専用の非スーパーユーザー役割を作成しましょう。これは重要なセキュリティプラクティスです。
-- Connect as a superuser (e.g., 'postgres') to run these commands
-- Create a new database for our platform
CREATE DATABASE wellness_corp_db;
-- Create a dedicated role for our application with login privileges
-- NEVER use a superuser for your application's day-to-day operations
CREATE ROLE wellness_app WITH LOGIN PASSWORD 'a_very_secure_password';
-- Grant the ability to connect to the new database
GRANT CONNECT ON DATABASE wellness_corp_db TO wellness_app;
-- Now, connect to the new database to continue setup
\c wellness_corp_db
-- Grant usage on the public schema to our app role
GRANT USAGE ON SCHEMA public TO wellness_app;
Expected Output:
CREATE DATABASE、CREATE ROLE、およびGRANTの確認が表示され、psqlプロンプトがwellness_corp_db=>に変わるはずです。
ステップ1:コアなマルチテナントスキーマの設計
実施内容
まず、プラットフォームのコアテーブルを定義します。重要な設計原則は、テナント固有データを含むすべてのテーブルにはorganization_idカラムが必要ということです。このカラムがマルチテナンシー戦略の基盤となります。
実装
組織(テナント)、ユーザー、ウェルネスエントリのテーブルを作成しましょう。
-- src/db/schema.sql
-- Stores our tenants (corporate clients)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Stores the employees of each organization
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, email) -- Email must be unique within an organization
);
-- Stores wellness data points for each user
CREATE TABLE wellness_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
entry_type TEXT NOT NULL, -- e.g., 'steps', 'mood_check_in'
value INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Grant our application role permissions on these tables
GRANT SELECT, INSERT, UPDATE, DELETE ON organizations, users, wellness_entries TO wellness_app;
動作の仕組み
organizations: このテーブルはテナントディレクトリですusers: 各ユーザーはorganization_idに直接リンクされています。ON DELETE CASCADEにより、企業のアカウントが削除されると、すべてのユーザーが自動的に削除されますwellness_entries: このテーブルにもorganization_idが含まれています。技術的にはusersテーブルに結合して組織を見つけることができますが、ここに重複させることでセキュリティポリシーが簡素化され、セキュリティチェックのための余分な結合を避けてパフォーマンスを向上させることができます
ステップ2:行レベルセキュリティ(RLS)の実装
実施内容
魔法が起こる場所です ✨。テーブルでRLSを有効化し、セキュリティポリシーを作成します。これらのポリシーはPostgreSQLによってすべてのクエリに適用されるルールです。ルールはシンプルです:「ユーザーは自分のorganization_idに一致するデータのみ表示できます」
実装
まず、PostgreSQLに対して特定のセッションでどのテナントが現在アクティブかを通知する方法が必要です。セッション変数を使用します。これは現在のデータベース接続の一時的な設定です。
-- In your application's connection setup, after a user authenticates:
-- SET session.current_organization_id = 'the-user-org-id-from-db';
次に、RLSを有効化してポリシーを作成しましょう。これらはスーパーユーザーまたはテーブル所有者によって実行される必要があります。
-- src/db/security_policies.sql
-- Enable RLS on the tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE wellness_entries ENABLE ROW LEVEL SECURITY;
-- Create a policy to allow users to see ONLY the data from their own organization
CREATE POLICY tenant_isolation_policy ON users
FOR ALL
USING (organization_id = current_setting('session.current_organization_id')::UUID);
CREATE POLICY tenant_isolation_policy ON wellness_entries
FOR ALL
USING (organization_id = current_setting('session.current_organization_id')::UUID);
動作の仕組み
ALTER TABLE ... ENABLE ROW LEVEL SECURITY;: このコマンドは指定されたテーブルのRLSをアクティブ化します。デフォルトでは「すべて拒否」ポリシーなので、許可的なポリシーが作成されるまで行は表示されませんCREATE POLICY ...:tenant_isolation_policyという名前のポリシーを定義しますFOR ALL: これはSELECT、INSERT、UPDATE、およびDELETEコマンドにポリシーを適用しますUSING (...): これはポリシーの核心です。既存の行が表示される(SELECT、UPDATE、DELETE用)ためには、この条件が必須です。行のorganization_idがセッション変数に設定したIDと一致するかどうかをチェックしますcurrent_setting('session.current_organization_id')::UUID: この関数はセッション変数を安全に取得し、比較のためにUUID型にキャストします
すべてをまとめる:実践的なテスト
RLSの動作を確認するためのテストを行いましょう。
1. サンプルデータの挿入(スーパーユーザーとして)
-- Insert two different companies
INSERT INTO organizations (id, name) VALUES
('11111111-1111-1111-1111-111111111111', 'Innovate Inc.'),
('22222222-2222-2222-2222-222222222222', 'Synergy Corp');
-- Insert users for each company
INSERT INTO users (organization_id, email, password_hash) VALUES
('11111111-1111-1111-1111-111111111111', '[email protected]', 'hash1'),
('22222222-2222-2222-2222-222222222222', '[email protected]', 'hash2');
2. テナントを設定せずにデータの選択を試みる
wellness_app役割として接続し、ユーザーを確認してみましょう。
SET ROLE wellness_app;
SELECT * FROM users;
Expected Output:
-- (0 rows)
成功です!RLSが動作しています。セッション変数を設定していないためポリシー条件が満たされず、データベースはテーブルが空であるかのように動作します。
3. 特定のテナントとしてデータの選択
次に、「Innovate Inc.」の認証済みユーザーになりすましましょう。
-- Set the tenant context for this session
SET session.current_organization_id = '11111111-1111-1111-1111-111111111111';
-- Now, run the query again
SELECT * FROM users;
Expected Output:
-- id | organization_id | email | ...
-- --------------------------------------+--------------------------------------+--------------------+----
-- ... | 11111111-1111-1111-1111-111111111111 | [email protected] | ...
-- (1 row)
完璧に動作しています!接続は現在「Innovate Inc.」にスコープされており、自分のデータのみ表示できます。Synergy CorpのBobは私たちには完全に不可視です。この分離はデータベースによって強制されるため、アプリ케이션レイヤーからの誤ったデータ漏洩のリスクが劇的に軽減されます。
ウェルネスプラットフォーム向けのセキュリティベストプラクティス
健康関連データの取り扱いには、追加の注意深さが必要です。主なセキュリティ考慮事項は次のとおりです:
- 暗号化: データ転送中は常にSSL/TLSを強制してください(接続文字列で
sslmode=require)。保存データの暗号化には、データベースサーバーのフルディスク暗号化を使用するか、AWS RDSなどの管理データベースサービスを活用してください - HIPAAコンプライアンス: PostgreSQLはHIPAA準拠にすることはできますが、デフォルトでは準拠していません。主な責任には以下が含まれます:
- アクセス制御: RLSの実装は強力な形式のアクセス制御です
- 監査ログ:
pgauditなどの拡張機能を使用して機密データへのすべてのアクセスと変更の詳細なログを作成します - データの匿名化: 分析用に、保護対象健康情報(PHI)の識別を解除するためにデータマスキングまたはトークン化テクニックを使用します。
pgcrypto拡張機能が役立ちます
- 最小権限: 作成した
wellness_app役割は、必要な最小限の権限のみを持つ必要があります。スーパーユーザーではなく、本番環境でテーブルを変更する権利を持ってはいけません
代替アプローチ
RLSはユースケースに最適ですが、代替手段を知っておく価値があります。
- テナントごとにスキーマ:
- 長所: より強力な論理的分離。単一テナントのデータのバックアップ/リストアが容易
- 短所: より複雑な移行スクリプト(すべてのスキーマをループする必要がある)。多数のアテナントでパフォーマンスオーバーヘッドが発生する場合がある。正しいスキーマを指すためにカスタムアプリケーショングルーティングロジックが必要
- アプリケーションベースの実施:
- 長所: 複雑なデータベース機能が不要。任意のデータベースで実装可能
- 短所: 非常にエラーが発生しやすい。数千のクエリの中の1つの忘れてられた
WHERE句が巨大なデータ漏洩を引き起こす可能性があります。すべてのコード行各行についてセキュリティの負担を разработчикиに置く
結論
スケーラブルな共有スキーマアプローチを使用して、B2Bウェルネスプラットフォーム用のマルチテナントデータベースを設計しました。PostgreSQLの行レベルセキュリティを活用することで、データの分離という重要な責任をアプリケーションバーからデータベース自体に移動させ、より安全で堅牢なシステムを作成しました。
このアーキテクチャは、共有インフラストラクチャのコスト面でのメリットを提供しながら、HealthTechのような敏感なドメイン必需的坚牢固なデータ保証をお届けします。
読者向けの次のステップ:
- ポリシーに関数を追加して、新しいデータが現在のテナントに対してのみ挿入できるようにすることを試してください
- JSONBを使用して
wellness_entriesテーブルに柔軟なウェルネスデータを保存することを検討してください - 包括的な監査証跡をセットアップするために
pgaudit拡張機能を調査してください
参考資料
- Official Documentation: PostgreSQL Row-Level Security Policies
- SaaS Architectures: AWS SaaS Tenant Isolation Strategies
クイックリファレンス
| 側面 | 主な情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と認識 |
| アクション | 医療提供者と相談する |
| 頻度 | ケアチームのおすすめに応じて |
| エビデンス | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当するのかどのようにわかりますか? A: あなたの個人的および家族歴について医療提供者と相談し、あなたの個々のリスク因子を評価し、適切な次のステップを決定してください。
Q: 医師に何と聞けばよいですか? A: 良い質問には次のようなものがあります:「結果は何を意味しますか?」「私のオプションは何ですか?」「どんなライフスチャンジが役立ちますか?」「いつフォローアップする必要がありますか?」
Q: 生活を改善するエクササイズはありますか? A: ほとんどの健康状態は、基本的な対策から恩恵を受けます:バランスの取れた栄養、定期的な運動(週150分以上)、質のよい睡眠(7〜9時間)、ストレス管理、および禁煙です。
医療免責事項
本文は教育目的のみを目的としており、医学的アドバイスを提供ものではありません。あなたの健康、治療、またはケアに関する決定をする前は、常に資格のある医療提供者と相談してください。個人の状況は異なり、1人に適切なことが別の人に適切でない場合があります。
緊急の医療問題が発生した場合は、直ちに119番(またはお使いの地域の緊急番号)に電話してください。