マイクロサービスのAPIをRESTからGraphQLへ段階的に移行した話

こんにちは。弥生の内山です。
この記事は弥生 Advent Calendar 2021 23日目の記事です。

qiita.com

はじめに

現在、弥生では新しいサービスの開発を行っており、今後主流となりうる技術を積極的に取り入れる方針で進めています。
本記事のタイトルにあるマイクロサービスアーキテクチャやGraphQLもそのようにして取り入れてきました。
ただし、それらの技術を実際にプロダクトに適用するのは一筋縄ではいかず、いろいろな遠回りを行った末にどうにか適用できた、というのが実際のところでした。
その過程については、前回12/16のもくテクにてお話させていただきました。

mokuteku.connpass.com

この過程の中で特に苦労したのが、既にREST APIとして構築したサービス群をGraphQL APIのサービス群へ移行させることでした。
もくテクの発表では、かなりの駆け足で移行の全体をお話ししたため、このGraphQLへの移行についてあまり詳しくご説明することができませんでした。
そこで本記事では、RESTからGraphQLへ移行した過程について、少し掘り下げてご説明したいと思います。

移行前のシステム構成

移行前のシステム構成は、以下の図のようになっていました。

f:id:ucho_yayoi:20211220173234p:plain

技術構成は以下のようになっていました。

  • フロントエンド:Next.jsを使用。API RoutesにGraphQLサーバーを立ててBFFとしている。
  • バックエンド:C#(ASP.NET Core)を使用。REST APIとして各種機能をフロントエンドに提供。

この構成において、BFFは以下のような役割を持っていました。

  • フロントエンドからバックエンドのアクセスを1箇所にまとめる
  • バックエンドが提供しているREST APIを集約・変換し、GraphQL APIの形でフロントエンドへ提供する

以上の構成において特に問題となっていたのが、BFFにてREST → GraphQLへのプロトコル変換を行う処理の実装が開発スピードを落としているということでした。
そこで、この部分を見直し、今後の開発スピードを高める策を検討しました。

移行方針

GraphQLは使いたい

フロントエンドとBFFの接続にはGraphQLを使用していましたが、これはそのまま使いたいと考えました。
GraphQLのメリットは様々なところで語られていますが、クライアント側で取得するデータを決定できるという特徴が、やはりフロントエンドとの相性が良いということを実感していました。
そこで、BFFより後ろのプロトコル変換部分を見直すことにしました。

プロトコル変換は無くしたい

プロトコル変換処理を作成するのは大変ですが、APIスキーマ等から自動生成を行う等の手段によって、ある程度はその労力を削減することはできると思います。
しかし、プロトコル変換を行うレイヤーが存在すること自体が、開発のボトルネックになるのではないかと考えました。
そこで、バックエンドサービスがREST APIを提供するのをやめて、変換後のGraphQL APIを直接提供するような構成にすることにしました。

移行のための技術選定

Apollo Federation

GraphQLは、クライアント側から見えるエンドポイントを1つにするのがベストプラクティスとされています。
各バックエンドサービスがGraphQL APIを提供するのであれば、BFFの位置でそれらをまとめて1つのGraphQL APIにすることが望ましいです。

複数のGraphQL APIを束ねて1つのGraphQL APIにしたい、というときに有力な選択肢となるのが、Apollo Federation仕様です。
Apollo Federationは、Netflixが採用したことで有名になった技術で、APIを束ねるゲートウェイが各APIの関連を理解して自動的にクエリの分配・結果の統合を行ってくれます。
現時点でGraphQLのマイクロサービス構成を作るならばこれだろう、と考え、Apollo Federationを採用することにしました。

www.infoq.com

zenn.dev

Apollo Federationでは、各APIが他のAPIとどのように関連しているかをゲートウェイに示すために、独自のスキーマ拡張仕様が定義されています。
この仕様をサポートしていないとゲートウェイにぶらさがることができないため、APIの実装にはApollo Federationに対応したライブラリを使う必要があります。

C#でのGraphQL(不採用)

さて、ではC#のサーバーで実装されたREST APIを、GraphQL APIに置き換えていこうと思ったのですが、ライブラリの選定時点で頓挫してしまいました。
C#のGraphQLライブラリで最も人気があったのはgraphql-dotnetですが、以下のような理由で我々にはマッチしていないと考えました。

  • APIが難しい
    • 非常に高機能な一方で、どう書けば目的を達成できるのかを理解するのが難しかった
    • 公式のドキュメントがあまり充実しておらず、Webにも情報が少なかった
  • Federationへの対応が中途半端
    • Federation対応のAPIを書くにはSchema Firstモードを使う必要があるが、Schema Firstモードの使い方に関する情報はさらに少なく、これを調査しながら使い続けるのは現実的ではないように感じた

ちなみに、一般的にGraphQL APIを定義する方法は大きく分けてSchema First(まずスキーマを作成し、それに対応するリゾルバを実装する)とCode First(クラス定義等からスキーマが生成される)があり、世の中の多くのライブラリはSchema Firstを採用しています。
どちらの方法も一長一短ですが、API実装者がAPI定義も行う場合は、Code Firstの方がスキーマ定義と実装との不一致が起こらないため、有効だと感じます。
なお、graphql-dotnetはSchema FirstとCode Firstの両方をサポートしているライブラリです。

また、graphql-dotnet以外には実用レベルに至っていると感じられるライブラリが見つけられませんでした。
このため、C#でFederation対応のGraphQL APIを実装することは困難であると考えました。

NestJS + GraphQL

C#での実装が難しいということがわかったため、他の選択肢を検討しました。
GraphQLのサポートが強力なのはやはりJavaScript/TypeScriptベースのライブラリでした。
その中でもNestJSは、Code FirstモードでFederation対応のAPI定義をサポートしているため、我々の要求にピッタリ合うという印象でした。

docs.nestjs.com

また、NestJSを採用すれば、フロントエンドもバックエンドもTypeScriptで開発できるようになるため、1つの機能を開発する際のオーバーヘッドが削減できそうです。
以上のことから、バックエンドの各サービスはNestJSを用いて開発することにしました。

お試し移行

しかし、既にC# + RESTで開発したAPI実装を、NestJS + GraphQLのAPIに変更するということは、つまりバックエンドサービス群を新しい技術スタックでまるごと書き直すということになります。
さすがにそれをいきなり実行するのはリスクが高すぎるため、まずは新規機能のために開発するAPIサーバーを、この新しい技術スタックで作成してみることにしました。

作成してみて、これまでの技術スタック(C# + REST、BFF内でGraphQLへ変換)で開発していたときと比較し、機能が完成するまでのスピードが明らかに早くなったと感じました。
単純に記述するコードの量が減りましたし、言語やプロトコルを頭の中で切り替える必要がなくなったので、作業効率もアップしたのだと思います。
この新しいGraphQL APIの実装方法に手応えを感じたため、これまでに作成してきた機能についても、新しい技術スタックへの置き換えを進めることにしました。

APIのマージ

さて、新しくGraphQL APIを立てたものの、このAPIを既存のシステムにうまく接合することができていませんでした。
この新しいAPIもBFFにぶらさげる形としたかったのですが、以下のような理由によりそれができませんでした。

  • Apollo ServerはREST API等の複数のデータソースを変換して1つのGraphQL APIにまとめることはできるが、GraphQLのデータソースはサポートしていなさそう
  • Apollo Gatewayは、複数のGraphQL APIをまとめることはできるが、GraphQL以外のAPIをまとめることはできなさそう

そのため、実は新しいAPIはフロントエンドから旧APIと別に呼んで、フロントエンドで結果をマージするという、かなり乱暴な暫定処置を行っていたのでした。
(新しいAPIとこれまでのAPIがそれほど結びつきが強くないということも幸いし、この処置でどうにかなったのでした)

しかし、このままではフロントエンドが複数のAPIエンドポイントを呼ぶ形になってしまい、複雑さが高くなってしまうのは明白です。
また、これまで作成したREST API群をGraphQL APIに置き換えていくときに、全てのAPIを一気にGraphQLにするのではなく、APIサーバーを順番にGraphQLに書き直してシステムに接合していけるような方法が採れないと、移行のリスクが高くなってしまうと考えました。
そこで思いついたのが、「REST APIをまとめてGraphQL APIにするサーバー」と、「各GraphQL APIをまとめるサーバー」の2段構成にするアイデアでした。

移行ステップ

以下のようなステップを経て、REST API群をGraphQL APIへ置き換えていきました。

1. REST APIを集約する部分を切り出す

今まではNext.js上のAPI RoutesにApollo Serverを配置し、これがREST APIを集約してGraphQL APIを提供していました。
このApollo ServerをBFFから切り離し、独立したサービス(API Gateway)として動作させるようにしました。

f:id:ucho_yayoi:20211220173237p:plain

2. GraphQLをまとめる

システム上に、2つのGraphQL APIサーバー(API Gatewayと、新規機能開発で作成したGraphQL APIサーバー)が存在する状態になるので、これらをApollo Gatewayを使って集約し、1つのGraphQL APIにします。
Apollo Gatewayは、いったんAPI Routesに配置します。
これによって、フロントエンド側からは1つのGraphQL APIだけが見える状態になります。
(新APIがフロントエンドに直結されてしまう問題もこれで解決できます)

f:id:ucho_yayoi:20211220173239p:plain

3. REST APIサービスをGraphQL APIサービスに移行する

この状態であれば、REST APIサーバーの1つをGraphQL APIサーバーに書き直しても、API RoutesのApollo Gatewayの下にぶらさげれば、フロントエンドから見たGraphQL APIとしては変化しないということになります。
これによって、REST APIサーバーをひとつずつGraphQL APIサーバーに書き直して、順次システムに接合する、ということが可能になります。

f:id:ucho_yayoi:20211220173242p:plain

4. API Gatewayを切り出す

REST APIサーバーをすべてGraphQL APIサーバーに書き直した後は、REST APIを集約していたApollo Serverが必要なくなります。
また、各Next.jsアプリのAPI Routesに置いたApollo Gatewayがほとんど同じことをやっているため、これらを1つにまとめたいです。
そこで、API GatewayにApollo Gateway(名前が紛らわしいですね)を配置するようにします。
ここまで実施すれば、GraphQLなマイクロサービス構成への移行が完了です。

f:id:ucho_yayoi:20211220173231p:plain

まとめ

こうして振り返ってみると、なんだか非常に遠回りをしたような気がします…。
しかし、結果としてすっきりした技術構成へ移行することができましたし、移行過程でGraphQLまわりに関する知見を深めることができたと思います。
この記事が、GraphQLへの移行を試みる方の参考になればうれしいです。

お知らせ

弥生では一緒に働く仲間を募集しています! herp.careers