Upgrade to Pro — share decks privately, control downloads, hide ads and more …

NestJS + Prisma2 で歩む RLS の世界

NestJS + Prisma2 で歩む RLS の世界

NestJS meetup Online #2 登壇資料
https://nest-jp.connpass.com/event/244015/

ynaka81

May 20, 2022
Tweet

Other Decks in Technology

Transcript

  1. 中川 裕太 @ Beatrust SRE
    NestJS + Prisma2 で歩む
    RLS の世界

    View Slide

  2. 今日の登壇内容
    Today's topic
    去年のちょうど今頃に運用を始めた NestJS + Prisma2 による RLS 適用の後日談です
    ※ 資料に載せているコードは概念的な簡易コードなのでそのままでは動きません

    View Slide

  3. Agenda
    RLS とは NestJS + Prisma2
    による RLS 実装
    性能問題
    の改善
    1 2 3

    View Slide

  4. 2020年12月に Beatrust にジョイン
    運用がラクにできるように色々と改善したり、セキュリティ
    向上したり、インフラ作ったり API 開発したりしています
    中川 裕太
    Yuta Nakagawa


    SRE & Tech Lead
    @ Beatrust
    @ynakaSoftTennis
    社員インタビュー https://note.com/beatrust/n/n24ac6848907b

    View Slide

  5. Beatrust は、会社や組織のなかで
    いっしょに働く人のことをもっと知り、助け合い、
    そして協業しやすい状態を実現するための
    タレントコラボレーション プラットフォームです。
    Beatrust が提供しているサービス
    What we do

    View Slide

  6. 創業以来のユーザー数の推移
    Growth
    2020 年 3 月
    Beatrust 創業
    2021 年 1 月
    1,000 名突破
    2021 年 10 月
    10,000 名突破
    2022 年 3 月
    15,000 名突破
    2020 年 3 月に創業して以来、
    1 年間で 1,000 名、
    2 年間で 15,000 名 のユーザーに
    ご利用をいただいています。
    ※ 2020 年 3 月から 2022 年 3 月までに Beatrust に登録された累積ユーザー数です。

    View Slide

  7. 導入企業とケーススタディの例
    Customer stories
    すでに日本を代表とする大企業での導入事例があり、実際の従業員による協業・活用事例が生まれています。
    普段社内の他部門を支援させていただく仕事を担当
    していますが、そうした日常業務の中で、実際に
    Beatrust が役に立ち始めています。
    生産性技術革新部 ビジネスプロセス支援グループ
    ご担当者様
    自分一人で悶々としているよりは話を聞いてもらっ
    てアドバイスをもらうことで気持ちを切り替えるこ
    とが大事だと強く実感できました。
    ビジネス開発センター ビジネスインキュベーショ
    ン ご担当者様
    社内にはまだ一度もお会いしたり、仕事をご一緒し
    たことのない方も多くいらっしゃいますが、少しで
    も人となりを知ることができ、心構えができて仕事
    がしやすくなっています。
    事業管理本部FHR・購買部
    ご担当者様
    国内総合証券会社 不動産デベロッパー コワーキングスペース
    大手製薬メーカー 大手通信会社
    大手自動車メーカー
    ほか、数多くの導入・トライアル実績(2022 年 4 月現在)
    医療機器メーカー
    大手通信機器メーカー
    大手広告代理店
    システムインテグレーター

    View Slide

  8. RLS とは NestJS + Prisma2
    による RLS 実装
    性能問題
    の改善
    1 2 3

    View Slide


  9. Multi-tenant
    in 1 database
    1 tenant
    in 1 database
    1 tenant
    in 1 instance
    database structure



    system cost



    migration cost



    security



    Multi-tenant でのデータベース構成

    View Slide


  10. Multi-tenant
    in 1 database
    1 tenant
    in 1 database
    1 tenant
    in 1 instance
    database structure



    system cost



    migration cost



    security



    コストの観点から multi-tenant in 1 database を採用

    View Slide

  11. アプリケーション側で
    tenant を WHERE 句で指定する必要がある

    View Slide

  12. ダルいしミスも発生しやすい

    View Slide

  13. そこで Row Level Security (RLS)
    PostgreSQL
    NestJS

    View Slide

  14. ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;
    CREATE POLICY "UserIsolationPolicy" ON "User" USING (
    "tenantId" = current_setting('app.current_tenant')
    );
    事前に table 単位でデータアクセスに対するポリシーを定義
    PostgreSQL
    NestJS

    View Slide

  15. 実行時設定パラメータ に tenant id を指定
    SET app.current_tenant = {tenant_id};
    PostgreSQL
    NestJS

    View Slide

  16. 事前定義したポリシーが正になるデータだけ取得
    PostgreSQL
    NestJS
    User WHERE "tenantId" = current_setting('app.current_tenant')

    View Slide

  17. アプリケーション側だけでなく
    DB レイヤーである種の WHERE 句をかけられる

    View Slide

  18. tenant を指定しないとデータ取得できないので
    fail safe な作りになる

    View Slide

  19. RLS とは 性能問題
    の改善
    1 3
    NestJS + Prisma2
    による RLS 実装
    2

    View Slide

  20. リクエストごとに独立したコネクションを張り tenant id を設定する
    PostgreSQL
    NestJS
    Client Side
    Prisma 2

    View Slide

  21. tenant 情報をリクエストの JWT から取得
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    tenant in JWT

    View Slide

  22. 新規にコネクションを張り tenant id を設定
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    SET app.current_tenant = {tenant_id};

    View Slide

  23. tenant id に従ったデータを取得
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    User WHERE "tenantId" = current_setting('app.current_tenant')

    View Slide

  24. Client にデータを返すと同時にコネクションをクローズ
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    User data Close connection

    View Slide

  25. これをどう NestJS + Prisma 2 で実装しているか

    View Slide

  26. PostgreSQL
    NestJS
    Client Side
    Prisma 2
    MW
    NestJS の Middleware と Interceptor で実装
    Interceptor

    View Slide

  27. PostgreSQL
    NestJS
    Client Side
    Prisma 2
    MW
    Middleware で Request Scope の ContextService に情報を格納
    Interceptor
    this.contextService.set('tenantId', jwt.tenantId)

    View Slide

  28. PostgreSQL
    NestJS
    Client Side
    Prisma 2
    MW
    Prisma 2 の Middleware を使って各クエリの前に SET を実行
    Interceptor
    @Injectable({ scope: Scope.REQUEST })
    export class PrismaService extends PrismaClient {
    constructor(private readonly contextService: ContextService) {
    this.$use(async (params, next) => {
    const tenantId = this.contextService.get('tenantId');
    await this.$executeRaw(`SET app.current_tenant='${tenantId}';`);
    return next(params);
    });
    }
    }

    View Slide

  29. PostgreSQL
    NestJS
    Client Side
    Prisma 2
    MW
    最後に Interceptor でコネクションをクローズする
    Interceptor
    @Injectable({ scope: Scope.REQUEST })
    export class PrismaInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable {
    return next.handle().pipe(
    finalize(() => this.prisma.$disconnect())
    );
    }
    }

    View Slide

  30. RLS とは NestJS + Prisma2
    による RLS 実装
    1 2
    性能問題
    の改善
    3

    View Slide

  31. RLS 適用してから数ヶ月は順調に運用できていました

    View Slide


  32. View Slide

  33. 次第に明らかになる性能問題...

    View Slide

  34. アクセスが集中すると異常に遅くなる

    View Slide

  35. 何が起きていたのか
    PostgreSQL
    NestJS
    Client Side
    Prisma 2

    View Slide

  36. コネクションを張る度に Query Engine のプロセスが立ち上がる
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    Query Engine
    ※ Prisma 3 ならデフォルトで Node-API Library として動作するので問題化はしない可能性は高いです

    View Slide

  37. プロセスなので当然実行時間は遅く全体に対して支配的
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    Query Engine
    < 100msec
    > 500msec

    View Slide

  38. そこで

    View Slide

  39. Query Engine でちゃんとコネクションプーリングするように変更
    PostgreSQL
    NestJS
    Client Side
    Prisma 2
    Query Engine

    View Slide

  40. つまり

    View Slide

  41. Prisma 2 の Client をグローバルなシングルトンとして実装
    PostgreSQL
    NestJS
    Client Side
    Global Prisma 2
    Query Engine
    MW
    Req Prisma 2

    View Slide

  42. Request Scope の Client はメソッドをパススルー + RLS
    PostgreSQL
    NestJS
    Client Side
    Global Prisma 2
    Query Engine
    MW
    Req Prisma 2
    constructor(conf?: RlsConfig) {
    this.user.findUnique = this._rebuildQueryMethod(
    client.user.findUnique
    );
    }
    private _rebuildQueryMethod(method: Function) {
    return (client
    .$transaction([
    this.$executeRaw(
    `SET app.current_tenant='${tenantId}';`
    ),
    Reflect.apply(method, client, args),
    this.$executeRaw(
    `SET app.current_tenant='';`
    ),
    ])
    .then((results) => results[1]));
    };

    View Slide

  43. tenant 情報は同様に Middleware で JWT から取得
    PostgreSQL
    NestJS
    Client Side
    Global Prisma 2
    Query Engine
    MW
    Req Prisma 2
    this.contextService.set('tenantId', jwt.tenantId)

    View Slide

  44. 10倍近い latency の改善

    View Slide

  45. まとめ
    Today's summary
    NestJS の Request Scope を使うことで
    リクエストごとのRLS を実装しセキュアなデータ操作を実現
    2
    3
    1
    性能問題は発生したが
    Prisma 2 が推奨するグローバルなシングルトンを作ることで解決
    ちょっと強引な方法でも
    フレームワークの思想にそった実装をするのが大事

    View Slide

  46. Beatrust on note Beatrust techBlog
    Beatrust のオフィシャルメディア
    Our official media
    Twitter
    @jp_beatrust
    Facebook
    LinkedIn
    note.com/beatrust
    社員のバックグラウンドや、
    働き方に関する記事をアップデート
    しています。
    tech.beatrust.com
    Beatrust の開発者チームによる取り
    組みと技術に込めた想いを掲載して
    いる開発者ブログです。
    Beatrust のメンバーに関する記事、開発メンバーの取り組み、最新ニュースリリースなどを随時更新しています。
    facebook.com/beatrust.official
    linkedin.com/company/beatrust

    View Slide