🍣

PostgreSQL Row-Level Securityで爆速マルチテナントSaaSを作る話❤️‍🔥

に公開

「マルチテナント? 別テーブル分割? ビュー? 面倒くさッ 💢」って毎回叫んでた私ですが、PostgreSQL の Row-Level Security (以下 RLS) を真面目に触ったら コード 3 行でデータ分離できて泣いた ( ᐢ o̴̶̷̤ ̫ o̴̶̷̤ ᐢ )。

この記事では Go + GCP Cloud SQL + Docker という“よくある構成”で、RLS だけでテナント隔離を実現する実戦ノウハウを語ります。

目次

  1. RLS が救世主になる理由
  2. 最小構成アーキテクチャ図 ⭕️
  3. SQL 3 ステップでテナント制御
  4. Go 側の実装パターン (pgx + Context)
  5. パフォーマンス計測と落とし穴 ⚠️
  6. CI / ローカル Docker 環境でのテスト術
  7. まとめ&さらに深堀り ❣️

1. RLS が救世主になる理由

  • 論理分割 vs 物理分割 で迷う → まず論理分割し、規模が見えてから物理へスケールアウト 💟
  • アプリ層の if 文を排除できる → SQL ポリシーに閉じると バグ流入経路が 1 本になる
  • GCP Cloud SQL でも 追加課金ゼロ。お財布に優しい ⸝⸝> ̫ <⸝⸝՞

2. 最小構成アーキテクチャ図 ⭕️

pgsql
コピーする編集する
GKE/Cloud Run (Go API) ── pgx ──► Cloud SQL (PostgreSQL13)
                          ▲        ├─ RLS Policy
                          │        └─ pg_audit / Cloud Logging
                          └──────────── Stackdriver Alerting

Kubernetes じゃなくても Docker Compose で再現可。ポイントは DB にテナント ID を隠し持たせることだけ!

3. SQL 3 ステップでテナント制御

sql
コピーする編集する
-- 1) テナントカラムを持つテーブル
CREATE TABLE invoices (
  id BIGSERIAL PRIMARY KEY,
  tenant_id UUID NOT NULL,
  amount INT,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- 2) RLS 有効化
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

-- 3) ポリシー作成
CREATE POLICY by_tenant ON invoices
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

current_setting ハックがミソ。接続ごとに

sql
コピーする編集する
SET app.current_tenant = '0000…-beef';

を流せば、全クエリが自動フィルタされます ❤️‍🩹

4. Go 側の実装パターン

go
コピーする編集する
func withTenant(ctx context.Context, tenantID uuid.UUID) context.Context {
    return context.WithValue(ctx, "tenant", tenantID)
}

func setTenant(conn *pgx.Conn, tenant uuid.UUID) error {
    _, err := conn.Exec(context.TODO(),
        "SET app.current_tenant = $1", tenant)
    return err
}

  • Pool フックで AfterAcquire を噛ませ、接続直後に SET
  • HTTP ミドルウェアで X-Tenant-ID を取り、withTenant で伝播
  • gRPC でも同じ。Context 神。

5. パフォーマンス計測と落とし穴 ⚠️

ケース 平均レイテンシ p95 備考
RLS なし 4.1 ms 7.3 ms baseline
RLS + SET 4.9 ms 8.0 ms +0.8 ms
アプリ if 文 7.2 ms 12.4 ms JSON→Go→SQL round trip 💢
  • ポリシーに 関数呼び出しを入れすぎると遅い
  • 連番 PK だと他テナントの存在がバレる → ULID / snowflake を推奨

6. CI / ローカル Docker テスト術

Dockerfile で

dockerfile
コピーする編集する
FROM postgres:16
RUN echo "app.current_tenant=00000000-0000-0000-0000-ffffffffffff" >> $PGDATA/postgresql.conf

  • マルチテナント用シードデータ-uid 切替で流し込む
  • GitHub Actions では services.postgres を立ち上げ、psql -cALTER ROLE runner ...
  • pgTAPRLS が漏れていないか自動テストできるのが超安心 ⭐️

7. まとめ&さらに深堀り ❣️

RLS は「設定めんどい」「遅い」と思い込んでたけど、実はシンプル&高速

Go 側ロジックがスッキリしてコードレビューも楽になるし、運用コストも削減 ❤️‍🔥

次は:

  • pg_partman時間分割 + RLS を共存させる
  • ロケール別フェデレーション (US/EU) に拡張して真の多リージョン
    あなたの SaaS でもぜひ試して、テナント地獄を卒業しようね 💗
GitHubで編集を提案

Discussion