😺

SaaS開発でもGraphQLを使いたい!

2024/03/26に公開

SaaSパーティショニングモデル

まず、SaaS環境でテナントデータを分割するときには、一般的に使用される3つの基本モデル (サイロ、ブリッジ、プール) があります。


AWSでのマルチテナントストレージモデルの構築より

3つのモデルの詳しい解説については、ここ数年で多くの記事/議論が上がっており、ここでは詳しく解説しませんが、SaaSというビジネスモデル上カスタマイズ性と引き換えにコスト面/生産性を優先してプールモデルを選択することが多く、弊社でも同じくプールモデルを採用しています。

またプールモデルを実現するためのデータベース戦略として、OSSのDBの中ではPostgreSQLのRLS(Row Level Security)機能が唯一の選択肢であり、実質的にデファクトスタンダードとなっています。(OSSではないがOracleやSQLServerでもRLS機能を有している。)

https://aws.amazon.com/jp/blogs/news/multi-tenant-data-isolation-with-postgresql-row-level-security/

GraphQL

GraphQLの採用

フロントエンド/バックエンド間のインターフェースとしてGraphQLを採用するケースが増えてきました。GraphQL-Codegenなどクライアント側ライブラリが豊富であること、1回のネットワークリクエストで効率的かつ柔軟にデータを取得できるなどのメリットがあります。特に大規模開発の際は、高い学習コストを払ってでも余りある成果が得られる場合が多いです。

PostGraphileの採用

RLS環境下でもGraphQLを使いたい!そんなモチベーションで技術選定した結果、PostGraphileに辿り着きました。
https://www.graphile.org/postgraphile/

PostGraphileはNode用のライブラリなので、Node用のバックエンドフレームワークに導入します。PostgreSQLと接続すると、データベース内のスキーマを読み取ってGraphQL APIを自動生成します。

上の画像のように基本的なQuery/Mutationに関しては自動でエンドポイントが生成されるため、データスキーマ=バックエンドエンドポイントを実現することができます。(ほとんどバックエンドエンジニアの出番ないじゃん!)

肝心のRLSに関してですが、JWTを介して行レベルの権限制約を付与することができます。
https://www.graphile.org/postgraphile/security/

実践編

実際に弊社での利用例を見てみます。認証認可にNext-Authを使用しているので、Next-AuthのStrategyとしてjwtを選択し、それを介してRLS機能を有効にしています。

const express = require("express");
const { postgraphile } = require("postgraphile");
const ConnectionFilterPlugin = require("postgraphile-plugin-connection-filter");
const { decode } = require("next-auth/jwt");

app.use(
  postgraphile(
    // DB接続
    {
      ...baseDBConfig,
      user: process.env.DBUserName,
      password: process.env.DBUserPassword,
    },
    "public",
    {
      watchPg: false,
      graphiql: true,
      enhanceGraphiql: true,
      exportGqlSchemaPath: "schema.graphql",
      simpleCollections: "both",
      ...postgraphileOptions,
    // ここまではPostGraphileのオプション
      pgSettings: async (req) => {
        try {
          const decoded = await decode({
            token: req.headers.authorization,
            secret: process.env.NEXTAUTH_SECRET,
          });

          return {
            "jwt.claims.tenant_user_id": decoded?.sub ?? "",
            "jwt.claims.tenant_id": decoded?.current_tenant_id ?? "",
          };
        } catch (error) {
          console.log("error", error);
        }
      },
    }
  )
);

こうすることで、RLSが有効になった状態でGraphQL APIエンドポイントを自動生成できます。

余談:他のGraphQL Serviceは?

類似するGraphQL EngineであるHasuraは、PostgreSQL由来のRLSではなく、認可(AuthZ)機能との組み合わせによって行のアクセス権を制御しているようです。
https://hasura.io/blog/row-level-security-with-postgres-via-hasura-authz

技術選定当時はHasuraがRLSに対応していない旨を明示していていましたが、現在の視点でもBtoB領域の厳しいセキュリティ要件を考慮すると、PostGraphileに軍配が上がりそうです。

使ってみてどうか

コミュニケーションコスト

データスキーマを定義すると、自動でバックエンドのGraphQLエンドポイントをはやしてくれるため、フロント/バックのコミュニケーションは大幅に削減することができました。

フロントエンド

JWTに関するやり取りはPostGraphile内で完結しているため、通常のGraphQL開発と同じく、GraphQLクライアント(弊社ではApollo Client)を介して型安全に開発を進めることができます。

バックエンド

単純なCRUDなどのボイラーテンプレートに関しては自動生成されるため、テーブル設計やコアなドメインに関する実装など、より高度な部分での領域にバックエンドエンジニアのリソースを割くことができます。
またバックエンドの言語はGolangですが、ORMに関してはRLSをコントロールしたいことから、DBファーストなORMであるSQLBoilerを使用しています。
https://github.com/volatiletech/sqlboiler

RLSを中心に考えることで、迷うことなくプロダクトに必要な技術を取捨選択できるようになりました。

まとめ

サンプルが少ないですが、BtoBマルチテナントSaaSを開発する際に、PostgresのRLS + PostGraphileはかなり開発体験がよく、この領域における次のデファクトスタンダードになるのではないかと思います。

フィシルコム

Discussion