こちらはバイセルテクノロジーズ Advent Calendar 2022の20日目の記事です。
前日の記事は富澤さんの「standaloneモードを利用してNext.jsのデプロイ速度を改善した話」でした。
こんにちは。テクノロジー戦略本部 開発二部の今井です。
私は現在、リユース領域でマルチテナントなサービスの開発に携わっています。
マルチテナントには色々な開発手法がありますが、今回はHasura GraphQL Engine(以下、Hasura)やAuth0といったサービスを活用して、マイクロサービスなマルチテナントのシステムを開発する方法をご紹介します。
※HasuraやAuth0の詳細はリンクにあります公式ドキュメントをご覧ください。
背景
自分が携わっているプロジェクトでは弊社内で利用される基幹システムを開発しており、複数のグループ会社で利用されるためマルチテナントとなっております。
また扱っているドメインが多岐にわたるためそれらを疎結合としたい・プロダクトごとに自由なアーキテクチャを採用できるようにしたいといった理由で、マイクロサービスとして開発しています。
そんなマルチテナントのシステムを開発するためにはサーバやDB、認証をどのように行うか設計する必要があります。
マルチテナントの設計方針
マルチテナントとは、サーバやDBといったリソースを複数のテナント(企業や個人)で共有する仕組みを指します。
またマルチテナントにおける設計ではサーバやDBといったリソースをどの粒度で共有するかを決める必要があります。
設計方針は大まかに2種類に分類されます。
①のモデルはテナントごとにDBを分離する設計です。
DBを分離しているため堅牢ですが、サーバを共有しているため、一部テナントの利用動向が全テナントに影響を及ぼすといったいわゆるノイジーネイバーの問題が起きる可能性によりスケーラビリティとトレードオフになります。
※詳しくは以下の記事にも記載があります。
②のモデルはサーバ・DBともに共有する設計です。
サーバとDBを共有しているため運用の効率化やインフラコストの削減がメリットとなりますが、データをテナントごとに分離できるかといった安全性やスケーラビリティとトレードオフになります。
どの設計方針を採用するかはビジネス要件によって変わってきますが、そもそもマルチテナントを採用する主な動機は効率性であるため、自分らのプロジェクトでも運用の効率化やインフラコストの削減等を考慮し、②のモデルを採用しています。
システム構成
では具体的なシステム構成やそれぞれの課題に対してどのように取り組んでいるのかを説明します。
自分たちのプロジェクトではインフラにGoogle Cloud(以下、GCP)を使用しています。
それぞれのドメインごとにGCPプロジェクトを用意し、フロントエンドとバックエンドのマイクロサービスを構築しています。
バックエンドにはPostgreSQLなどといったDBエンジンのスキーマからGraphQL APIを生成できるOSSのHasuraを採用しています。
認証にはIDaaSのAuth0(認証認可の機構にはJWT)を採用し、フロントエンドとバックエンドの通信には内製しているAPI Gatewayを仲介するような構成になっています。
またAPI Gatewayではすべてのマイクロサービスの認証を担保しており、JWTの検証やログの集約等も行っています。
かつAuth0がもつJWTのRevoke情報も管理しているため、JWTのRevoke検証も行っています。
考えられる課題は多くありますが、ここでは以下3点に着目して話を進めていきます。
- テナントやユーザを適切に管理できるか
- Hasuraを利用してテナントごとにデータを分離できるか
- スケーラビリティを担保できるか
Auth0との連携でテナントやユーザを適切に管理
まずはマルチテナントに対応するうえで、テナントやユーザ等を適切に管理できるかといった課題があります。
そういった課題に対しては「ユーザ管理」をドメインとしているマイクロサービス(以下、ユーザ管理サービス)を構築することで解決しています。
このサービスでやっていることとしては主に以下2つです。
- テナントや利用ユーザそれぞれに付与するロールをマスタデータとして管理
- Auth0 Management APIを利用したユーザデータの追加更新
※管理画面や処理は一部抜粋したものであり、実際とは異なる部分がありますのでご了承ください。
ユーザ管理サービスでは、テナントやロールをマスタデータとして管理し、各テナントに紐づくユーザを追加したい場合は管理画面上より氏名やロール等を保存できるようになっています。
またAuth0にはAuth0 Management APIというユーザの登録や更新等といったCRUD操作のできるAPI群が提供されています。
そのため管理画面上での保存をトリガーに、ユーザのメタデータを保存する領域であるapp_metadataにtenant_idやrole_idを追加し、ユーザ登録APIを実行します。
こうすることでユーザ管理サービス上でもユーザやロール等の管理ができ、かつ後述する認証認可の機構として採用しているJWTのclaimにtenant_idやrole_idをカスタムすることも可能になります。
HasuraとカスタムしたJWTを組み合わせてテナントごとにデータを分離した状態を実現
続いてHasuraを利用してテナントごとにデータを分離できるかといった課題に言及します。
全テナントでDBを共有する場合「他のテナントにデータが共有されてしまう・漏洩してしまう」といった問題があります。
それらの対応として以下を採用しています。
- Auth0 Actionsを利用してJWT claimにtenant_idを付与
- HasuraのRow-Level Permissionsでtenant_idによる権限制御を設定
Auth0にはAuth0 Actionsというカスタムロジックを用意できる機能があります。
ユーザのメタデータには事前にtenant_idやrole_idといった情報を付与しているため、ログインした際JWT claimにそれらを付与するよう設定します。
かつHasuraではJWTに格納されているx-hasura-*
の値を、自動的にアクセスコントロールのルールとして適用します。
そのためtenant_idをx-hasura-tenant-id
キーの値として設定することで、後述するアクセスコントロールを実現できます。
※ソースコードは一部抜粋したものであり、実際とは異なる部分がありますのでご了承ください。
続いてHasura側の設定です。
ここでは例としてusersテーブルを用いて説明します。
tenant_idカラムではテナントごとに割り振る一意のIDを管理している想定です。
またJWT claimに存在するx-hasura-tenant-id
の値とusers.tenant_id
の値が同一であることを条件とした参照権限を設定します。
クライアントにてAuthorizationヘッダーにAuth0より取得したトークンをセットしてGraphQLクエリを実行することで、それぞれのテナントに所属するユーザのみ参照できます。
またHasuraのロジックを拡張する用途としてRemote Schemaを採用しています。
Remote Schemaの実態は独立したGraphQLサーバであり、queryやmutation、認可処理を自前で実装する必要があります。
ただHasuraで認証したJWTをRemote Schemaに転送できるため、認証をやり直す必要はなく、JWTに格納されているx-hasura-tenant-id
を使用して制御が可能です。
こうすることでHasuraの利便性を享受しながら、データを分離した状態を実現できます。
API Gatewayを拡張することでスケーラビリティの担保にも貢献
続いてスケーラビリティを担保できるかといった課題について言及します。
現状では安全性については解決できても、前述したノイジーネイバーの問題でスケーラビリティを担保できていません。
こちらの対応としては、前述したAPI Gatewayの機能を拡張することで対応していこうと考えています。
仮に将来的に一部テナントより大量のリクエストが発生したとしても、設定した上限を超えたリクエストを制限しHTTPステータスコード429を返すなどといったスロットリング機能を実装できます。
これによりリソースの占有やサービスが停止してしまうこと等を防ぐことができます。
※詳しくは以下の記事にも記載があります。
またAPI Gatewayではリクエストの転送も実装しているため、新規テナントの増加や新規マイクロサービスを用意する必要があった場合でもルーティング処理によって対応できます。
こうすることで一部テナントの利用動向に左右されず、スケーラビリティを担保できます。
まとめ
今回はマルチテナントなマイクロサービスを開発するうえで、HasuraやAPI Gatewayをどのように活用するかについて紹介しました。
同じようにHasuraを利用している、またはマルチテナントに対応するプロダクトを構築しているといった方に参考となりましたら幸いです。
最後にBuysell Technologiesではエンジニアを募集しています。
興味がある方はぜひご応募ください。
明日のバイセルテクノロジーズAdvent Calendar 2022は杉田さんの「手動送金地獄をシステム化して作業時間を50%削減した話」です。そちらもぜひ読んでみてください。