高速な仮説検証を支えていくHasuraの導入

ogp

はじめに

eScanチームの平田です。

eScanは日本の電力事業に特化したリスク管理サービス(ETRMシステム)です。2022年から開発を始め、お陰様で今では徐々に導入いただける社数も増えてきています。まだまだ不足している機能はありますが、すべてのお客様が確実に必要とするであろうETRMの必須機能の実装は概ね終わっており、新規機能追加中心フェーズは終わりつつあります。これからは、よりお客様にとって価値のあるサービスになるような機能強化や改善を中心にする必要があると考えています。

今回は、eScanチームでの事業フェーズの変化に伴う開発プロセスの試行錯誤と、高速な仮説検証の補助として導入を進めているHasuraについてお話します。

フェーズの変化と仮説検証の必要性

「はじめに」に記載したとおり、徐々に新規機能追加中心のフェーズからより顧客への提供価値の最大化を目指すフェーズに差し掛かっています。eScanは開発当初からドメインエキスパートと密に連携し、顧客の声も聞いてきましたが、これからはこれまで以上に素早く顧客やドメインエキスパートのフィードバックを得て、プロダクトに反映する必要があります。そうしなければ、時間をかけて的外れなものを作ってしまう恐れがあります。そうなればせっかく作ったものもお客様のプロダクトの活用やプロダクトの価値の最大化に繋がらず、チャーンに繋がってしまう可能性があります。

後述しますが、Google Spreadsheetを活用して実装を簡易化し、素早くフィードバックを得る取り組みも行いました。しかし、プロダクトとして提供する際の解像度には及ばず、適切なフィードバックにはつながりにくかったため、最終的にはプロダクトとして動く状態を早期に見せることが重要だと感じました。

従来のアーキテクチャと課題

eScanのアーキテクチャは過去のブログでも紹介していますが、以下のようなものです。

image1-tech-stacks

  • 言語
    • TypeScript
  • ウェブアプリケーションフレームワーク
    • NestJS
  • ORM
    • Prisma
  • backend↔frontend間の通信プロトコル
    • GraphQL
  • バックエンドのソフトウェアアーキテクチャ
    • オニオンアーキテクチャ

このアーキテクチャにより、振る舞いやビジネスルールの多い箇所はドメインを隔離し、DBやPresentationの都合と切り離して実装やテストがしやすくなっているというメリットは享受できていると思います。しかし、シンプルなAPIを作る場合でもgraphql-nestjsのDTO、Data Loader, Resolver, Repository, NestJSのModule, UseCase, etc…、と多数の記述が必要でした。 初期の仮説検証フェーズではデータベースのデータを少し整形して返すようなシンプルなAPIで十分な場合が多い中で、このオーバーヘッドは本当に受け入れる必要があるものだろうかと問い直しました。 *1

様々なアプローチの模索

この問題に対して、他にも様々な取り組みを行ってきました。一部を紹介します。

レイヤリングをせず手数の最小化

PoCの実装に限り、厳密なレイヤリングを行わずに最速でリリースすることを許容しました。例えばpresentation層のGraphQL Resolverから、直接DBからデータを取得して返してしまうなどです。(その後のリファクタリングは前提なのでAPIの振る舞いのテストを用意することは必須ですが)

モジュールの隔離

上の延長線上ですが、レイヤーの無視をあちこちで行うと、単に割れ窓が大量に発生する恐れがありました。 そこで、最速で価値検証するための実装についてはPoCモジュールとして切り出し、その中でのみ実装速度に特化した実装を許容しました。 このモジュールは検証期間を決めて、期間終了後にはリファクタリングして通常のモジュールに統合していくことをルールとしています。

これらの取り組みはいずれも素早く実験することに貢献したものの、後のリファクタリングでコストがかかったり、検証が長引きPoCモジュールに実装が取り残されたままということがありました。

アプリケーションとして実装せずに見せる

リスクやポジションの計算と可視化が中心の機能ではそもそもアプリケーションとして実装しないで見せるということも行いました。例えばGoogle Spreadsheetに機能を実装してデモを行いフィードバックをもらうというものです。

計算のロジックを詰める際にはこの方法はかなり活躍しました。Google Spreadsheetにロジックを実装し、ドメインエキスパートとシートを見ながら、妥当なロジックになるよう検証と改善を繰り返しました。 ロジック部分についてはこの手法が良いと思いましたが、アプリケーションとしての見せ方は実物の動くアプリがあって初めて深まる部分が多かったです。やはり最速でアプリとしてリリースすることが重要であると感じました。

様々な取り組みを模索する中で、もっとロジックやUIの重要な実装にフォーカスするにはどうすればいいかと考えていった結果、Hasuraと出会いました。

Hasura導入の検討

Hasuraはデータベースから自動的にGraphQLのAPIを提供するツールです。

単純なAPIの実装に時間が取られてしまうという我々の課題にマッチしているのではないかと思い、検討を始めました。*2

Hasuraの導入の目的は、(少なくとも直近は)仮説検証を高速に行うための実装の高速化です。読み込み系のAPIはHasuraに任せて、複雑なドメインロジック等は今まで通りバックエンドのアプリケーションで実装する予定です。

技術要件・検証ポイント

Hasuraを導入するにあたって検討した要件や検証したポイントをいくつか紹介します。

クラウドかセルフホストか

Enterprise PlanであればSLAはcustomizableとされていますが、外部要因によってサービスが落ちることを避けたかったこと、まだ検証フェーズであることから、セルフホストを選択しています。運用コストはかかってしまいますし、OSS版では制限されている重要な機能もあるため今後はクラウドを利用していくことも検討していくと思います。

認証認可・セキュリティ

enechainでは共通の認証基盤があるため、その基盤と統合できることが必須の要件でした。社内の基盤はJWTを利用して認可するため、JWTを自由に扱うことができれば概ね目的は達成できます。 幸いHasuraはJWTを柔軟に取り扱うことができます。我々の認証基盤が発行する独自のclaimでも、claims_mapを使って十分に対応できます。 また、レイテンシは増えてしまいますがWebhookが利用できるため、社内で提供されている認証認可の共通ライブラリを利用してendpointを実装することで、社内の基盤との統合は問題なく実装できます。

また、複数の顧客のデータを保持していることから顧客間のデータの分離が重要です。GraphQLでは柔軟にクエリを書くことができるため、迂闊にすべてを外部に露出させると、思わぬデータの漏洩に繋がってしまう可能性があり、この部分の実装は慎重になる必要があります。

この点Hasuraでは以下のようにロールベースでテーブル・行・列ごとに権限を管理できます。他にもリクエストごとに取得できる行数や集約クエリの許可など、柔軟に権限の設定ができます。

hasura-permission1

hasura-permission2

参考: https://hasura.io/docs/2.0/auth/authorization/roles-variables/

ロールごとに取得できる条件を設定できるので、JWTから取得した顧客の企業IDを元に、当該企業の情報しか取得できないというのをデフォルトの条件に設定することで、他の企業の情報にアクセスできてしまうことを防げます。今までもPostgreSQLのRow Level Securityなどは用いておらず、アプリケーションの実装時に必ず組織IDを条件に加えることで制御していました。Hasuraではテーブルは権限を明示的に設定しなければ閲覧できないので、設定時に顧客での絞り込みをデフォルトにすることで今までと同等またはそれ以上に安全に実装できそうです。

既存のAPIと共存できること

先述の通り、すべてがシンプルなCRUDで表現できるわけでもないので、何もかもHasuraで行うのは無理があります。主にMutationは今まで通りバックエンドのサーバで実装していく予定でいるため、既存のAPIと共存して利用できる必要があります。

ここでは以下の2つの課題がありました。

  1. HasuraのRemote Schemaで統合するか、エンドポイントを分けたままにするか
  2. GraphQLのクライアントをうまく生成できるか

HasuraのRemote Schemaで統合するか、エンドポイントを分けたままにするか

HasuraにはRemote Schemaという機能があり、データベースだけでなく既存のGraphQLサーバをデータソースとして取り込むことができます。これで統合できれば話は速かったかもしれませんが、我々の場合は

  • PostgreSQLのenumを多用していること
  • 既存への影響を少なくするため、prefix等を付けるならHasuraで生成する側にしたい

という点が足枷となっています。

HasuraはPostgreSQL nativeのenumの利用は推奨しておらず、native enumはGraphQLのScalarとして生成されてしまいます。

(推奨される方法は単一カラムのテーブルを作り外部キー制約をつけること。この方法であればGraphQLのenumとして生成される。)

Scalarとして生成されてしまうだけであればクライアントの生成時に型をうまく付け替えてなんとかできたかもしれません。

ところが、Hasuraはprefixを設定していてもScalarにはprefixを付与しません。その結果、Remote Schemaで統合する既存のAPI側にprefixを付けない限り、どうしても型が衝突してエラーになってしまいます。(データベース側でScalarが生成され、Remote Schema側にenumで同名の型が存在するため。)Remote Schema側にprefixを付けると既存のクエリをすべて書き換えなくてはならないのでそれは避けたいところです。

そこで、当面はエンドポイントを分割したまま運用する方針を取りました。

Hasuraがデータベースから生成するスキーマにはprefixは付けておき、enumを単一列のテーブルに移行していくことでRemote Schemaへの移行もできる想定です。しかし、バックエンドの実装時にはPrismaで型安全にenumを扱えることによる恩恵もあるので、バランスを取りながら進めていくことになりそうです。

GraphQLクライアントの生成

GraphQLクライアントの生成はgraphql-codegenを使っています。エンドポイントを分けたためクライアントの生成は2つのスキーマを別々に生成することになり、注意すべきことは減りました。

HasuraでGraphQLクライアントを生成する際に気をつけることは「HasuraとGraphQL Codegen(TypeScript)のこだわり設定」に詳しく書かれており、参考にさせていただきました。

運用

スキーマの管理

OSS版のセルフホストを選択しているため、自前で運用する必要があります。eScanはGKE上で稼働しているため、Hasuraも同様にコンテナで動かしています。Kubernetes上で動作させることにはそれほど大きな障害はなかったため、ここではデータベースのスキーマ等メタデータの運用にフォーカスします。

eScanではデータベーススキーマはPrismaで管理しています。Prismaが生成するクライアントは開発で重要なので、Hasuraにもスキーマを管理する機能は備わっていますがHasura導入後もこの運用は変えずに運用できることが望ましいです。

Hasuraはデータベーススキーマ等のメタデータのimport/exportができるので、以下の流れで既存の管理方法は変えずに運用できます。

  • Prismaでスキーマの管理
  • ローカルではHasuraのダッシュボードを利用して追加・変更したスキーマの情報を反映
  • Hasuraのメタデータをexportし、gitにコミット

CDパイプラインの中でデプロイ前にデータベーススキーマのマイグレーションを行っていますが、そこでHasuraのマイグレーションも加えることでスキーマの整合を保つことができます。

スキーマの変更時の対応

ダイニーにおける本番 Hasura 運用」にもありますが、Hasuraでは直接データベーススキーマがGraphQLのインターフェースになるので、変更が難しいのではないかという懸念がありました。

直近eScanチームでは互換性を保つリリースを重視しており、Hasuraを利用していなくてもスキーマの変更は互換性を保ちながらリリースすることを基本としているため、大きな問題にはならないと判断しました。

パフォーマンス

パフォーマンスに関しては、それほど厳しい要件はありません。十分なレベルのクエリが生成されていると判断しています。また、Hasuraのダッシュボードで開発時に生成されるクエリやその実行計画を確認できるので、問題のあるクエリが生成されていないか確認しながら開発を進めることができます。

モニタリング

パフォーマンスのメトリクスやログを収集して、サービス横断で使っているDatadogでモニタリングがしたいです。

HasuraはPrometheusとOpenTelemetryの連携機能を持っていますが、Self-hostedではEnterprise限定のため活用できていません。 現時点ではHasuraのログはDatadogに連携されているため、ログに出力されるquery_execution_timeなどの情報から必要なメトリクスを抽出しています。

traceを統合できていないのは今後の課題です。

他社事例

事例がないからと言って使ってはいけないというわけではありませんが、長く実戦投入している事例があるかどうかで安心感は変わってきます。国内ではダイニーさんやBuySell Technologiesさんが利用しており、多くの情報を発信しているため、参考にさせていただきました。

導入による効果(見込み)

まだ本格的な運用に組み込めていないため、効果は見込みの段階ですが、検証時点で実感していることを書きます。

実装工数の削減

ちょっとした検証をするだけでもたくさんの実装をしなければならなかったのが、フロントエンドでGraphQLを書くだけで済んでしまい、重要なことにフォーカスできています。今後利用範囲を拡大していくと効果が大きくなっていくと期待しています。

仮説検証サイクルの短縮

検証段階ではMutationも許容することで、データベースのスキーマさえ用意すればフロントエンド側の実装だけで検証のための実装を完結できます。今まではデータベースまで繋がって動作するものを見せるまでには時間がかかってしまっていましたが、これによりほぼロジックだけに集中して見せられるプロダクトを構築できるようになっています。

クエリの検証も容易に

おまけとして、クエリや実行計画の確認がHasuraのダッシュボードで完結するのでクエリの検証が少しだけ簡単になりました。

まとめと今後の展望

eScanの仮説検証をより高速化する施策として、Hasuraの導入を検討している経緯をご紹介しました。まだ本格運用までは至っていないため、実際の成果は来年のブログで改めてお知らせできればと思っています。 今後は、これまで多く実装してきたAPIをHasuraへ移行することで、アプリケーション側が重要なロジックへ注力できるよう工夫を重ねつつ、Hasuraとのバランスを探っていく予定です。

今回の取り組みはプロダクト開発改善全体の一部分にすぎませんが、引き続き顧客へ最大限の価値を提供すべく、継続的な改善を進めていきたいと思っています。

最後に

enechainでは、一緒に働く仲間を募集しています。興味を持っていただけた方は、ぜひ以下のリンクから詳細をご覧ください。

tech.enechain.com

herp.careers

*1:アーキテクチャやフレームワークそのものの問題ではなく、我々の開発方法に問題があることは認めます。

*2:余談ですが、eScanチーム内には当初からこの点を問題視していたり、前職でHasuraを活用していた人もいました。それでも、「Hasuraを試してみたらええんちゃう?」という声は、なかなか上がりませんでした。ただ、チーム全体のカルチャーを改善する取り組みを並行して進めたことが、今回の成果につながったと思っています。この詳細は明日、同じくeScanチームの小沢さんからご紹介します。