Successfully reported this slideshow.
Your SlideShare is downloading. ×

RLSを用いたマルチテナント実装 for Django

RLSを用いたマルチテナント実装 for Django

Download to read offline

RLSを用いたマルチテナント実装 for Django
by Takayuki Shimizukawa

複数のテナント(チーム・組織)向けにサービスを提供するシステムで、テナント相互の情報を分離して扱う、複数のマルチテナントアーキテクチャが考案されています。「各プログラマが努力して実装する」戦略でも実現はできますが、プログラミングミスや設定間違いによるデータ混濁が高確率で発生します。このトークでは、マルチテナントアーキテクチャにおけるデータ分割アプローチのひとつ「共有アプローチ」をDjangoとPostgresのRow Level Security (RLS) の組合せで安全に実現する方法を紹介します。またこの方法のメリット、デメリットを紹介します。
https://djangocongress.jp/

RLSを用いたマルチテナント実装 for Django
by Takayuki Shimizukawa

複数のテナント(チーム・組織)向けにサービスを提供するシステムで、テナント相互の情報を分離して扱う、複数のマルチテナントアーキテクチャが考案されています。「各プログラマが努力して実装する」戦略でも実現はできますが、プログラミングミスや設定間違いによるデータ混濁が高確率で発生します。このトークでは、マルチテナントアーキテクチャにおけるデータ分割アプローチのひとつ「共有アプローチ」をDjangoとPostgresのRow Level Security (RLS) の組合せで安全に実現する方法を紹介します。またこの方法のメリット、デメリットを紹介します。
https://djangocongress.jp/

More Related Content

More from Takayuki Shimizukawa

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

RLSを用いたマルチテナント実装 for Django

  1. 1. Takayuki Shimizukawa 2021/7/3(土) DjangoCongress JP 2021 RLSを用いたマルチテナント実装 for Django 1
  2. 2. @shimizukawa (清水川) ● BeProud 取締役 / IT Architect ○ 受託開発(Webアプリ / 機械学習 / 数理最適化) ○ 自社サービス(connpass / PyQ / TRACERY) ○ Python研修(Python基礎、Django、Pandas、その他) ● 一般社団法人PyCon JP Association 会計理事 ○ PyCon JP 年次イベントの見守り ○ Python Boot Camp 主催 ● Sphinx (コミッター休業中) おまえ誰よ / Who are you 2
  3. 3. 1版: 2010/5/28 2版: 2018/2/26 3版: 2021/7/30 1版: 2013/09/12 2版: 2017/10/20 2018/02/23 2020/02/27 1版: 2012/03/27 2版: 2015/02/27 3版: 2018/06/12 執筆・翻訳した書籍 3 NEW 年
  4. 4. このトークで紹介すること マルチテナントアーキテクチャにおけるデータ分割アプロー チのひとつ「共有アプローチ」を、PostgresのRow Level Security (RLS)を用いてDjangoで安全に実現する方法を紹介 します。 またこの方法のメリット、デメリットを紹介します。 4
  5. 5. アジェンダ ● マルチテナント概要と課題 ● RLS: Row Level Security ● DjangoではRLSどうなるの? ● Djangoデモ ● マルチテナントの開発と運用 ● まとめ ● 参考資料 5
  6. 6. マルチテナントの概要と課題 6
  7. 7. マルチテナントの概要 マルチテナントは、複数のテナント(チーム・組織)向けにサービスを提供 するシステムです。 ● マルチテナント ○ 1つのシステムに複数の顧客 ○ 顧客同士はデータが隔離されている ○ 顧客同士は存在を知らない ● 対義語: シングルテナント ○ 1つのシステムに1つの顧客 ● それ以外: ○ 複数の組織/グループがサービス上共存している 7
  8. 8. Multi-Tenant Magic: Under the Covers of the Force.com data Architecture より 8 シングルテナントアーキテクチャ ユーザー企業ごとにデータベースを用 意すると、データベースごとに管理者 が必要となるため多くのシステム管理 者が必要となり、またインフラの利用 効率をあげることも難しい。 マルチテナントアーキテクチャ 全ユーザーが1つのインフラを共有する ため管理者が少なくて済み、インフラ の稼働効率も高くなる
  9. 9. マルチテナントの課題 マルチテナントによって、運用時の費用を下げたい。 しかし導入においては、テナント相互の情報分離、つまりデータ混濁が発生 しないと保証されていることが大事です。 「各プログラマが努力して実装する」戦略でもマルチテナントを実現できま すが、努力だけでデータ混濁を避けるのはとても難しいです。 9 小 難易度 大 大 運用コスト 小 図は Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する より引用
  10. 10. データ混濁 提供するデータに期待しないデータが混ざり込むこと。 ある顧客データのなかに別の顧客データが混ざりこむ等。 ● 個人情報が他の会社に見えちゃった ● 売上額の集計値に他の会社の売上が混ざる マルチテナントアーキテクチャは、データ混濁を避けるため の戦略 10
  11. 11. マルチテナントアーキテクチャ データ混濁を避ける、複数のマルチテナントアーキテクチャが考案されています 。 マルチテナント SaaS パターン - Azure SQL Database | Microsoft Docs による分 類では、マルチテナントは以下の3つのモデルに分けられる。 1. スタンドアロンアプリ 2. テナント単位データベース 3. シャードマルチテナント 図は Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する より引用 11
  12. 12. マルチテナントアーキテクチャ 1. スタンドアロンアプリ(DB分離、アプリ分離) ○ DB・アプリもテナント毎に用意、ユーザーには極めて柔軟 ○ 運用コストはもっともかかる 2. テナント単位データベース(DB分離/準分離、アプリ共有) ○ DBは分けつつ、アプリは共有 ○ 運用は、アプリが分かれてない分楽 ○ (清水川所感:Web記事、スライド等ではこのパターンが多い) 3. シャードマルチテナント(DB共有、アプリ共有) ○ 同じDB・アプリのリソースを利用 ○ 最も効率的で「真のマルチテナント」と呼ばれる ○ (今日紹介するのはこの方式) 12
  13. 13. DB分割アプローチ DB層でデータ混濁を避けるための、3つのDB分割アプローチ 1. 分離(isolated) ○ DBを分ける(≒ シングルテナントDB) 2. 準分離(semi-isolated) ○ DBは1つ、スキーマ/名前空間を分ける 3. 共有(shared) ○ DB/スキーマを分けない 13
  14. 14. DB準分離アプローチの特徴 ● 1つのDBインスタンス内に、テナント別スキーマ ○ ただし、一部のテーブルはPublicスキーマに配置 ● 仕組みが複雑で、通常運用が基本大変 ● アプリからは、スキーマ指定でDBにアクセス ○ Django拡張いくつかあり 採用例は多いが、保守停止時間はテナント数に比例 ● マイグレーション 30秒 x 5,000テナント = 42時間 ● 例: つらくないマルチテナンシーを求めて: 全て見せま す! SmartHR データベース移行プロジェクトの裏側 DB準分離アプローチ 14
  15. 15. DB共有アプローチ DB共有アプローチの特徴 ● DB、スキーマが全体で1つ ● 仕組みがシンプルで、通常運用が基本楽 ● WHERE句にTenantIDを指定必須 ○ Django拡張はあまりない 採用を避ける理由 ● ミスると、データ混濁が発生 ○ ORM実装で、WHERE句付け忘れ ○ 保守で生SQLを書いて、WHERE句付け忘れ ● 直感に反する(分かれてないので、危なそう) 15
  16. 16. DB分割アプローチ, Pros, Cons 5000テナントの場合の例 (django-easy-tenants · PyPIを元に追記) 16
  17. 17. メリット ● テーブルがスキーマ単位で分かれている(データ隔離) ○ データ混濁が発生しにくい、複数スキーマのデータが混在しない ● テナント個別の制御がしやすい ○ バックアップ、リストアが容易 ○ 負荷が高いテナントがあるとき、DB分離に切り替えやすい デメリット ● テーブルがスキーマ単位で分かれている(同じテーブルがたくさん) ○ 各スキーマにマイグレーションが必要、時間はテナント数に比例 ● テナント個別の制御が必要 ○ 管理ユーザーが、テナントそれぞれに必要 ○ 管理コストがテナントの数だけかかる DB準分離アプローチのメリデメ 17 by @kashew_nuts
  18. 18. メリット ● スキーマはひとつ、テーブル構造もひとつ ● テナント個別の制御が不要 ○ 管理ユーザーは、テナントをまたいで管理しやすい ○ 大抵はマルチテナントを忘れられるため、通常の実装 & 管理コストが低い デメリット ● テナント個別の制御がしづらい ○ バックアップ・リストア ○ 特定テナントだけをメンテナンス ○ 個別カスタマイズ ■ 保守運用が死ぬので、どのアプローチでもやめましょう!JSON型カラムで回避 ● ミスった時のデータ混濁が致命的 ○ RLS: Row Level Security (行レベルアクセス制御)を導入しましょう! 18 DB共有アプローチのメリデメ by @shimizukawa
  19. 19. DB準分離 vs DB共有 19 django-tenantsのような拡張も公開 されてますし! コードレベルで変更が必要な箇所は 分かってます! 準分離はテナント数が増えた場合の運用コス トが大きい! DB共有なら省リソース・省コストで、RLSを 使えば安全に実現できるはず! (真のマルチテナント、やってみたいなー)
  20. 20. RLS: Row Level Security
  21. 21. RLS: Row Level Securityとは ● Row Level Security = 行レベルのアクセス制御 ● データベース層でデータ混濁を回避する技術 ○ SQL実行時に、アクセス権限のある行だけが返される ○ WHERE句の指定は不要 ○ アクセス権限は、DB接続ユーザー または ロール で決まる RLSを使えば、 「シャードマルチテナント(真のマルチテナント)」 を安全に実現できる 21
  22. 22. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 SELECT * FROM customers; RLS利用時は権限のある行のみ取得 22 SQL実行時に、WHERE句を指定しなくても、アクセス権限の ある行だけが返される。 tenant_id =2 の全行 つまり、アプリ各所でのSQL書き換えが不要 ■User ■Role ■RLS
  23. 23. 23 どういう仕組み?
  24. 24. db=# CREATE TABLE rls (id SERIAL PRIMARY KEY, tenant_id INTEGER); db=> INSERT INTO rls (tenant_id) VALUES (1),(2),(3),(1),(2),(3); db=> SELECT * FROM rls; id | tenant_id ----+----------- 1 | 1 2 | 2 ... 6 | 3 (6 rows) 実験用テーブル rls を作る 24 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3
  25. 25. db=# CREATE ROLE “1”; -- tenant_id=1 の行のみアクセスできるROLE(予定) db=# SET ROLE “1”; db=> SELECT * FROM rls; ERROR: permission denied for table customers db=# RESET ROLE; -- 接続ユーザー権限に戻る 新規のロール "1" を作る 25 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK NG ■User ■Role ■RLS https://www.postgresql.org/docs/13/sql-set-role.html USER でも ROLE でも可能ですが、ROLE で制御する 理由は29スライド付近で説明します。 PostgreSQLのロール - Qiita
  26. 26. db=# GRANT ALL ON ALL TABLES IN SCHEMA public TO ”1”; db=# GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ”1”; db=# SET ROLE “1”; db=# SELECT * FROM rls; 1 | 1 2 | 2 ... 6 | 3 (6 rows) db=# RESET ROLE; -- 接続ユーザー権限に戻る "1" に全権限を付ける 26 全権限があるので、全行見えてます rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK OK ■User ■Role ■RLS https://www.postgresql.org/docs/13/sql-grant.html
  27. 27. db=# ALTER TABLE rls ENABLE ROW LEVEL SECURITY; db=# SET ROLE “1”; db=# SELECT * FROM rls; id | tenant_id ----+----------- (0 rows) db=# RESET ROLE; -- 接続ユーザー権限に戻る rls テーブルのRLSを有効化 27 SELECT権限はありますが、 RLSによって制限されました rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role OK OKだが 1行も見 えない ■User ■Role ■RLS https://www.postgresql.org/docs/13/ddl-rowsecurity.html
  28. 28. db=# CREATE POLICY foo ON rls USING(tenant_id::text = current_user); db=# SET ROLE “1”; db=# SELECT * FROM rls; id | tenant_id ----+----------- 1 | 1 4 | 1 (2 rows) rls テーブルのRLSポリシーを設定 28 rls テーブル id tenant_id 1 1 2 2 3 3 4 1 5 2 6 3 Postgres users “db” User “1” Role SELECT権限がありますが、 RLSポリシーによって 対象行が制限されました OK OK ■User ■Role ■RLS https://www.postgresql.org/docs/13/ddl-rowsecurity.html
  29. 29. DjangoではRLSどうなるの? 29
  30. 30. tenant=2 で ログイン 2 dbで接続 セッションでのSELECT 30 全行 ■User ■Role ■RLS customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 Postgres users “db” User “1” User “2” User “3” User SELECT * FROM customers;
  31. 31. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 Postgres users “db” User “1” User “2” User “3” User セッション+RLSでのSELECT 31 CREATE POLICY foo ON customers USING(tenant_id::text = session_user); 接続Userで制御 ■User ■Role ■RLS SELECT * FROM customers; tenant_id =2 の全行 “2” で接続 ログインユーザー毎に、 DjangoからDBへ接続する ユーザーを切り替えるのは難しい
  32. 32. tenant=2 で ログイン 2 tenant_id =2 の全行 Postgres users “db” User “1” Role “2” Role “3” Role dbで接続 SET ROLE “2”; SELECT * FROM customers; RLS+Roleを介したSELECT(Role密結合) 32 CREATE POLICY foo ON customers USING(tenant_id::text = current_user); CREATE ROLE “1”; GRANT ALL ON TABLES ... TO “1”; GRANT ALL ON SCHEMAS ... TO “1”; -- 2, 3, 以下繰り返し customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 Roleで制御 ■User ■Role ■RLS ログインユーザーのHTTPリクエスト毎に、 Roleを切り替えるMiddlewareを用意 DjangoからDBへの接続は、 常に1つのuser/passwordを使う
  33. 33. customers テーブル id name tenant_id 1 customer_1 2 2 customer_2 2 3 customer_3 3 4 customer_4 1 5 customer_5 3 6 customer_6 1 7 customer_7 1 8 customer_8 2 tenant=2 で ログイン 2 Postgres users “db” User “tenantuser” Role “1” Role “2” Role “3” Role dbで接続 SET ROLE “2”; SELECT * FROM customers; RLS+Roleを介したSELECT(Role疎結合) 33 CREATE POLICY foo ON customers USING(tenant_id::text = current_user); CREATE ROLE “tenantuser”; GRANT ALL ON TABLES … TO “tenantuser”; GRANT ALL ON SCHEMAS … TO “tenantuser”; GRANT “tenantuser” TO "1"; -- 以下繰り返し Roleで制御 ■User ■Role ■RLS テナントのRoleは、代理ロール ”tenantuser” 経由でテー ブルへのアクセスが許可される tenant_id =2 の全行
  34. 34. Django でRLSを使うには(まとめ) 34 初期化 1. 代理ROLEを作成し、全テーブルのGRANT ALLを設定 2. テナント制御するテーブルにRLS有効化&ポリシー設定 都度 1. CREATE ROLE -- テナント追加時 2. SET ROLE -- テナント判別し、ROLEを設定 ■User ■Role ■RLS
  35. 35. 1 & 2. 初期化 35 db=# CREATE ROLE “tenantuser”; db=# GRANT ALL ON ALL TABLES IN SCHEMA public TO ”tenantuser”; db=# GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO ”tenantuser”; ※ 初期化は、後から追加されたテーブルにも個別適用が必要 ■User ■Role ■RLS -- for <table> in <tables_to_enable_rls>: db=# ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; db=# CREATE POLICY <name> ON <table> USING(tenant_id::text = current_user); 1. 代理ROLEを作成し、全テーブルのGRANT ALLを設定 2. テナント制御するテーブルにRLS有効化&ポリシー設定
  36. 36. 3. CREATE ROLE -- テナント追加時 36 ■User ■Role ■RLS from django.db.models.signals import post_save def on_create_tenant(sender, instance, created, **kwargs): if created: # レコード追加時 tenant_id = instance.tenant_id with connection.cursor() as cursor: cursor.execute(f'CREATE ROLE "{tenant_id}"') cursor.execute(f'GRANT "tenantuser" TO "{tenant_id}"') # DBにレコードが追加された後に実行する post_save.connect(on_create_tenant, sender=models.Tenant) https://docs.djangoproject.com/ja/3.2/topics/signals/
  37. 37. 4. SET ROLE -- Middlewareでテナント判別 37 ■User ■Role ■RLS https://docs.djangoproject.com/ja/3.2/topics/http/middleware/ class RlsMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): tenant_id = getattr(request.user, 'tenant_id', None) if tenant_id: # テナントに所属しないスーパー管理者はROLE設定不要 with connection.cursor() as cursor: cursor.execute(f'SET ROLE "{tenant_id}" ') response = self.get_response(request) return response ※ Middlewareを通らない処理では個別に SET ROLE が必要
  38. 38. 4. SET ROLE -- Middleware以外でもテナント設定 38 ■User ■Role ■RLS https://django-tenants.readthedocs.io/en/latest/use.html#tenant_context @contextmanager def tenant_context(tenant_id: int): with connection.cursor() as cursor: cursor.execute(f'SET ROLE "{tenant_id}" ') Yield cursor.execute(f'RESET ROLE') # バッチや非同期呼び出し等、Middlewareを経由しない場合の呼び出し def some_command(tenant_id): with tenant_context(tenant_id): # SET ROLE される objs = DjangoModel.objects.all()
  39. 39. デモ 39
  40. 40. マルチテナントの開発と運用 40
  41. 41. 各RDBMSのRLS対応状況 2021/05 調査時点 ● PostgreSQL 9.5 (2016) 以降 ○ https://www.postgresql.org/docs/9.5/ddl-rowsecurity.html 設定方法などのサンプルコード付き ● SQL Server 2016 (13.x) 以降 ○ https://docs.microsoft.com/ja-jp/sql/relational-databases/security/row-level-security?view=sql-server-ver15 ● Oracle 10g Release 2 (10.2) (2005) 以降 ○ https://docs.oracle.com/cd/B19306_01/network.102/b14267/intro.htm ● MySQL ○ 8.0.25 (2021-05-11) では非対応 ● MariaDB ○ 10.5.10 (2021-05-07) では非対応 ● SQLite ○ 3.36.0 (2021-06-18) 非対応 41
  42. 42. シングルテナント < マルチテナント ● 分離 = シングルテナントの場合 ○ データ混濁について考える必要はない ● 準分離(スキーマ分離)アプローチにした場合 ○ 共有するテーブルと分離するテーブルを管理し、専用のマイグレーション手順を用意 ● 共有アプローチにした場合 ○ 複数テナントを前提にしたテーブル設計が必要、RLSの導入が必要 開発コスト 42 DB分離アプローチ DB共有アプローチ
  43. 43. テナント数の増加に合わせて変化 ● 運用では、テナント数と管理対象が比例する ● DBやアプリのインスタンス毎に担当者が必要 運用コスト 43 少 テナント数 多 大 DB数 小 大 運用・改修コスト 小 大 リソース 小
  44. 44. ● パフォーマンス測定を行い、N+1等は解消しておく ○ RLSにより負荷は上がるため、顕著になってしまう ● 作り方によってはINDEXが効かない等の問題がある ○ ROLE名によるPOLICY適用時に、フルスキャンが発生等 ● コネクションをテナント別に用意しない(できない) ○ そもそもDBの接続数上限を100や1000にするのは厳しい(デフォルトは10) ○ Postgresは、コネクション生成コストが高いため、都度生成は負荷が高い ○ データ混濁を避けたいとしても、コネクションは共有するしかない ○ (なお、Djangoではテナント別コネクション利用がそもそも難しい) ● Citus / Hyperscale の利用を検討する ○ 特定カラムの値(tenant_id等)をキーに、ノード分散によって負荷分散できる RLS利用時の負荷対策 44
  45. 45. ● ROLE設定を忘れないようにする ○ 今回のデモではMiddlewareで自動設定した ○ 管理コマンド等Middlewareを通らないケースがあるので、そこは実装で努力 ○ ROLE設定を忘れると、「全テナントのデータが漏洩する」 ■ SET ROLEを忘れたら全部見れない方が安全 -> 今後の課題 ● バックアップをテナント単位でやりたい ○ DB全体のバックアップで。リストア方法は要検討 ● PITR(Point In Time Recovery)をテナント単位でやりたい ○ DB内の特定レコード群だけを巻き戻す、のは難しい。 ○ DB準分離ならスキーマ単位で独立のため、多少楽(publicスキーマのデータとの不整合に注意) ● 高負荷テナントのリソースを隔離をしたい ○ Citus / Hyperscale の利用を検討する ■ 特定カラムの値(tenant_id等)をキーに、ノード分散によって負荷分散できる RLS/DB共有のデメリット対策 45
  46. 46. まとめ RLSのメリット ● コンピューターリソースを無駄なく最大限活用できる ● マイグーレーション時間が線形増加しない ● テナント個別の特別対応をしない、という潔さ RLSのデメリット ● クエリ負荷の上昇は避けられない ● テナント個別の特別対応が難しい ○ バックアップ、リストア、負荷の高いテナントの分離 46
  47. 47. 参考資料 マルチテナント ● マルチテナント SaaS パターン - Azure SQL Database | Microsoft Docs ● Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する IBM Developer Works (WebArchive) ● Multi-Tenant Data Architecture Microsoft (WebArchive) ● つらくないマルチテナンシーを求めて: 全て見せます! SmartHR データベース移行プロジェクトの裏側 / builderscon 2018 - Speaker Deck RLS ● Django + RLS でマルチテナント - 清水川のScrapbox /デモコード https://github.com/shimizukawa/django-pg-rls ● Using Postgres Row-Level Security in Python and Django ● 行レベルのセキュリティ - SQL Server ● PostgreSQL の行レベルのセキュリティを備えたマルチテナントデータの分離 | Amazon Web Services ブログ ● Row Level Securityはマルチテナントの銀の弾丸になりうるのか / Row Level Security is silver bullet for multitenancy? - Speaker Deck ● PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring) Citus (Hyperscale) ● Azure Database for PostgreSQL Hyperscale(Citus)しらべてみた - Qiita ● チュートリアル:マルチテナント データベースを設計する - Hyperscale (Citus) - Azure Database for PostgreSQL | Microsoft Docs 47
  48. 48. Special Thanks! -- Review頂いたみなさん BeProud slack ● kashew, delhi09, susumuis, tell-k, mtb_beta, altnight django-ja Discord ● hirokiky, tokibito 48
  49. 49. and… Thanks for all attendees!! by @shimizukawa 49

×