銀の弾丸は存在しないHasura編

こんにちは、aisaacで新規事業のエンジニアをしている松山です。

今日は以前こちらの記事で紹介したHasuraについてです。

tech.aisaac.jp

hasura

便利な一面もありますがやはり辛い一面もありますので 検証していった時の気づきをまとめていきます。

また、Hasuraの良い点は色々な方が共有していたりドキュメントを見ることで把握することが簡単なので 今回はあまり表に出てこない、よくないところフォーカスして共有をしていきます。

前置き

※注意 実際に使ってみて難しいなって思ったところとドキュメントを見てこれは難しいなってなったところがあります。特にHasura Cloudはおそらくそうだろうみたいなところがあるのでご了承ください。

目次

  • Hasuraを使う目的
  • Hasuraを使う構成の検討
  • 難しい点

Hasuraを使う目的

何事も目的は大事!目的に沿っていなければ、時には切り捨てる勇気が必要だと勝手に思っています。この辺aisaacでは最低限揃える技術スタックがあるものの試して捨ててが容易なものに関してはすごく寛容なのでありがたいです!

改めての紹介ですが、aisaacでは新規事業が複数走っているので、開発においてスピードを優先させる場面があります。

詳しくはこちらの記事をご参照ください。 tech.aisaac.jp

Hasuraを使う構成の検討

hasura_architecture
hasura_architecture

案1. Hasura Cloudを使用して、キャッシュはHasuraCloudに任せる。

Pros

  • Remote Schema(Apollo FederationのようなものをHasuraが自前で作ってくれている)を使って、Schemaを一つにまとめることができる。
  • キャッシュの機能を自前で作り込まなくて良い。

Cons

  • NestJsでもキャッシュを使用すると、2段階にキャッシュしてしまって、キャッシュのパージなどは難しそう。
  • RemoteSchemaをHasura自体がInMemoryにキャッシュするので、NestJsで何かGraphQLに変更を加えるたびにキャッシュの消し込みデプロイが必要。

案2. BFFでbodyをhash化してRedisやMemcacheにキャッシュ

Pros

  • ApolloFederationの機能でできることが実現できる。

Cons

  • HasuraのApolloFederation連携のVersionは低い。独自のRemoteSchema(案1)に力を入れているように見える。
  • 初手で3つのサーバーのメンテナンスをしなければいけない。工法互換性を保ったり、デプロイ順序を制御したりが必要。

案3. NestJsなどでApolloFederationと独自の実装を共存させる。

これは、調査の結果できないことがわかりました。現状のところApollo Serverの仕様で、ApolloFederationを有効にして独自のGraphQLを定義するということができません。

構成のまとめ

GraqphQLを使用して、新規事業で俊敏性を損なわずにHasuraを導入するには案1しかなさそうというのがわかってきます。 人件費を考えたときにもコスパの良いのはやはりHasuraCloudでしょう。

それでもHasura Cloudではなくて、Hasura Community Editionを一旦検討されたい方のためにもう少し噛み砕いて記載していきます。

Hasuraと会社のセキュリティ要件での難しさ

HasuraはCommunityEditionを使うか、HasuraCloudを使うかで運用の方針は変わってくると思いますが、 基本的には最初からHasuraCloudを使うこと前提でいたほうがいいと思います。 その上で会社が何かしら、Privacyマークを取得していたりIsmsを取得している場合にはその基準を満たせるか(外部のクラウドにサーバーを置いて良いかどうかやその通信経路)をか考えたほうが良さそうです。

※ こちらは前職でセキュリティ要件が厳しい環境で過ごしていた個人的な見解です。

デプロイとHasuraサーバー単体で持つキャッシュのクリアの難しさ

Hasuraを実際に使うまで全然知らなかったのですが、HasuraのGraphQLモードは結構いろんなキャッシュをサーバー内に持ってしまいます。

  • GraphQLのリモートスキーマのキャッシュ
  • Enumテーブルのキャッシュ

GraphQLのリモートスキーマのキャッシュ

案1 でGraphQLスキーマを使う時にはHasuraRemoteSchemaを有効にするのが良さそうとなりましたが、RemoteSchemaは初めてHasura使う人を100%つまづかせます。

基本複数のサーバーを立ち上げるときはデータベースに近いものから立ち上げ(migrationしていく)と思います。下記に番号を振った順番です。

hasura_remote_schema
hasura_remote_schema

2と3は、具体的にどのように依存しているかというと、GraphQLのSchema Introsection これを使用することで、このGraphQLサーバーには、どのようなQueryやMutationがあるのかを伝えることができます。(RESTでいうOpenAPIのドキュメンテーションです。)

問題は、Hasuraが立ち上げ時にNestJsからGraphQLのスキーマを読み取ってキャッシュすることです! パフォーマンス的な問題とのことですが、これHasura未経験の人は100%つまづいて質問がくるのでちゃんとオンボーディングやたほうがいいですね。

github.com

hasure_remote_schema_strategy
hasure_remote_schema_strategy

ということは、これはNestJsのGraqphQLに後方互換性のない変更が加わった時には次のことを考慮してデプロイフローを考えていく必要があります。。。。

  • NestJsをデプロイした後Hasuraをデプロイする
  • NestJsとHasuraの間のデプロイ時間に呼び出されたらエラーになるので極力デプロイ時間を短くする。
  • スキーマが作られてしまう以上全てのサーバーで後方互換性を維持したAPIの開発をする。
  • もしくは、エラーが出ることを許容して後方互換性のないものは、どこかでスキーマが変わったら全てを再デプロイする。(キャッシュも消す)
  • 全体のBlue Greenをする仕組みを作って切り替える。

このような仕組みを作る必要があります。

Enumテーブルのキャッシュ

Hasuraには2通りで、Enumを使う選択肢があります。

  • PostgresのNative Enumを使う方法。
  • 単列テーブルへの外部キー参照の使用

PostgresのNative Enumを使う方法。

PostgresのNative Enumとは別の枯れたやり方で安定して運用ができるのが証明されている反面、これを採用すると運用に制約をつけてしまうのでお勧めはしません。

詳しくは同ブログの別記事にて記載予定です。

単列テーブルへの外部キー参照の使用

この方法は、通常のリレーショナルデータベースの概念を用いて列挙型を表現するものである。列挙型はテーブルで表現され、列挙型の値はテーブルの行になります。列挙型を使用する他のテーブルの列は、列挙型テーブルへの通常の外部キー参照である。 タグのリストやユーザーの役割のように、動的で更新が必要な値を持つ列挙型では、この方法を強く推奨します。この方法で定義された列挙型を変更するのは簡単です。列挙型テーブルの行を挿入、更新、削除するだけです(更新や削除は参照にカスケードすることもできますし、トランザクション内で実行することもできます)。

これは一見して良さそう。GraphQLのEnumも作ってくれてTypeScriptの型保管も効く。 ただ問題は、これもHasuraのサーバー内部でSQLが実行されてそれをキャッシュするそうです。 たぶん、SELECTをキャッシュして値のバリデーションなりに使用しているのではないでしょうか?実際のところどうかはわかりません。

SELECT * FROM EventStatus

ってことは、EventStatusになんらかの変更があった場合に、

データベースのマイグレーション -> 他のAPIのデプロイ -> hasuraのデプロイ

みたいなデプロイをしないといけないです。

enumへの個人的なまとめ

APIがいろんなシステムによって呼び出されて組織的にも、API開発チームがProviderとして機能する必要があればGraphQLのEnumを使いたいですが、ひとつのチームで完結するAPIはどこまでやるのかはチームの判断でいいと思います。

(ちなみに私は要件にあってたらGraphQLの機能をフルで使うことにとらわれないで良いと思う派です。なので/shareというディレクトリを作って、そのディレクトリ以下のファイルをapiとfrontで共有していたりします。)

セキュリティ周り(Hasura AdminSecret周り)の難しさ

HasuraのAdminSecretとセキュリティ

HasuraはAdminSecretという仕組みを使って、セキュリティを担保しているらしいです。 で何が難しいのかというと、AdminSecretはオフにすることができません。

前職でJ-SOXの対応などをやってきた自分、そして参画している以上はリスク軽減策なしで、リスクのある状態でアプリケーションを構築するというのはかなりの心理的な抵抗があるのが正直なところです。

パッと思いつくものは下記のものでしょう。

  • Ratelimitを設定する。(Hasura Cloudなら最初からついている。CEは自前実装。。。)
  • AdminSecretをローテーションする(HasuraCloudであれば複数のAdminSecretを設定可能なので無停止でRotationできるはず。)
  • HasuraをVPC内部に配置する。

Rate limitやRotationはそれぞれ実際の挙動を把握して運用マニュアルを作りつつ実際に運用するなどが必要です。

HasuraをVPCに置く場合は、たとえばNext.jsはVercelにおいて、VercelからHasuraを叩くようにするには、VPC内部にHasuraを配置するのはできなかったりします。 ただ、PublicにHasuraでアクセスできるDBの範囲をpublicに公開しているのと同じになるのでそれを念頭において必ずリスク軽減策を用意してほしいです。

また、Hasuraをフロントから直接叩きたいという時もVPCに置けません。GraphqlQL Subscriptionを使いたい時もそうですね。。プロキシしてやってもいいのですがそこまでくるとHasuraを使うかどうかからよく考えていきたいところです。

HasuraとNestJsとApolloとSPAの難しさ

案1 の構成をとった時に個人的に妥当かなと思っている使い方としては、NestJsで書き込み。Hasuraは読み込みたいな分け方です。読み込みのAPIを作らない分、素早く機能提供ができそうです。

ただその場合も特定の条件では、うまく機能しないことがあります。 SPAでApolloのInMemory機能を使った体験を構築する時です。

Apolloは、GraphQLのスキーマごとにキャッシュを持ってくれるのでSPAでサクサクした挙動を作る時には便利です。 ただ、読み込みがHasuraのGraqphQLのスキーマで、書き込みがNestJsのスキーマだと、別々のスキーマを使っていることになるのでApolloの良さがあまり発揮されません。(本来であれば、書き込みしたときに返却されるスキーマ情報ですでに同じスキーマでよみとっていたらそこが自動的に更新されるのですが、、、)

SEOを重視するためにSSRを使用している場合は、ページ単位で基本呼び出すと思いますので、問題になることは少なそうです。

付録: HasuraCloud周り

Price

hasura_cloud_price
hasura_cloud_price

キャッシングの仕組み

リソース単位ではなくで、エンドツーエンドのアプリケーションキャッシングとあるので、キャッシュ効率は低いと思います。 そして、エンドツーエンドのキャッシュはキャッシュのパージがしにくいのも難点です。。。

hasura_cloud_cache_1hasura_cloud_cache_2
hasura_cloud_cache

まとめ

Hasuraはプロダクトとそのプロダクトのフェーズによっては、すごくハマることがある一方でやってみると難しいなって思うところも多いです。今回は使用する上でこれ難しいなっていう観点であつめてみました。 どなたかの参考になれば嬉しいです。


PR!

👇 aisaac に興味をお持ちの方は、こちらの記事もぜひ。

tech.aisaac.jp

note.com

note.com

note.com

👇 コーポレートサイト、最近リニューアルしました 💯

aisaac.jp