近年のSaaSはより一層、マルチプロダクトを展開する企業が増えています。複数のプロダクトを展開していくことでプロダクト間のシナジーを生み出すことが期待されますが、その一方でプロダクト間の連携は非常に難しい課題です。アンドパッドでも、複数のプロダクトを展開していき成長を続けていく中で、その都度プロダクト間の連携をする仕組みを構築してきました。
そして、最近私が所属しているリアーキテクティングチーム1では、現在取り組んでいる中の1つのプロジェクトとしてこのプロダクト間連携を再設計してプロダクト間連携用のgRPC APIサーバーである「Tsugite」2を構築するプロジェクトを進めています。著者の@ydahはこのプロジェクトにおいて、プロダクトオーナー3としてプロジェクトを推進しています。
この記事では、我々がプロダクト間連携を進める上で考えたことや、何を考えてどのように進めてきたか、そしてこれからどのような未来を見据えているかを紹介します。 なお本記事はANDPAD Advent Calendar 2024の24日目の記事です。他の記事も是非ご覧ください。
この記事を読む前に
この記事では、どのようにして我々がプロダクト間連携を進めてきたか、その過程でどのように考えたか、そして現在地と未来について紹介します。 大統一プロダクト間連携の銀の弾丸を見つけたから紹介するというようなものではないので、その点をご理解いただいた上でお読みいただければと思います。
序章: プロダクトの発展、連携の必要性
ANDPADは2016年のサービス提供開始当初、建築事務所と現場をつなぐ施工管理サービスの提供からスタートしました。 その後、提供サービスの多様化に伴い、図面の管理、検査項目や進捗状況の見える化、受発注業務のサポートなど、さまざまなプロダクトを開発・提供してきました。
当初、ANDPADはモノリシックなRailsアプリケーションとして運用されていましたが、プロダクトの多様化に伴い状況は変化しました。開発速度を重視し、本体とは別に新規Railsアプリを立ち上げて進めるという方針が取られました。
近代では、Vertical SaaSへの本格的な移行を進めるため、新規プロダクトを積極的にリリースするフェーズに移行しています。この流れの中で、我々はマイクロサービス化を試み、各サービスごとに技術選定し、Goで実装されるプロダクトも増加しています。
これにより、各プロダクトはそれぞれ独立したデータを持つ一方で、ユーザーや共通データはモノリシックなRailsアプリケーション側で一元管理され、各サービスはgRPCを通じて連携するgRPC APIサーバーが整備されました。しかし、プロダクト間連携にはさらなる改善の余地が残されていました。
たとえば、モノリシックなRailsアプリケーションとの連携方式が複数存在することです。REST APIとgRPC APIが混在しており、用途によって使い分ける必要がありました。この状況では、プロダクト間連携する際にどちらのAPIを使用するか選択し、各方式で実現可能な内容を把握し、不足があれば新たなインターフェイスを追加する必要がありました。
さらに、Goで実装されたgRPC APIサーバーがモノリシックなRailsアプリケーションのデータにアクセスするために提供されています。このサーバーは、モノリシックなRailsアプリケーションのDBを直接参照するシンプルな設計となっており、以下のメリットがありました:
- 複雑なモノリシックRailsアプリに依存せず、迅速に機能を提供できる
- 高速なレスポンスを実現
しかし、この設計には以下の課題もありました:
- モノリシックRailsアプリ側のビジネスロジックを利用しないため、ロジックが分散し、複雑なデータモデルの扱いが困難
- モノリシックRailsアプリのDBスキーマが変更された際、gRPC APIサーバー側でも追従が必要になる可能性がある
これらの課題を解決し、プロダクト間連携をより円滑にするための取り組みが求められるようになり、こうして僕たちの大統一プロダクト間連携プロジェクトが始まりました。
第1章: 第一歩、連携の試み
プロダクト間連携を進めるにあたり、最初に行ったのは現状の把握と、大統一プロダクト間連携の必要性を検討することでした。
まず、現状把握として複数存在する連携方式を整理し、それぞれの特徴や利点、欠点を洗い出しました。また、プロダクト間連携を実際に行っているチームへのインタビューを実施し、連携の現状や課題をヒアリングしました。
その結果、モノリシックなRailsアプリケーションが管理する共通データの取得方法が一意に定まっていない点が、大きな課題であることが明らかになりました。この課題を解決する必要性が確認されたため、我々はプロトタイプを作成し、パフォーマンスの検証を開始しました。
既存のGoで実装されたgRPC APIサーバーは、直接DBを参照することで非常に高速なレスポンスを実現していました。そのため、新たに開発する「Tsugite」では、この高速性を可能な限り維持しつつ、モノリシックなRailsアプリケーションのビジネスロジックを活用することが求められました。パフォーマンスが不十分であれば、プロダクトとして成立しない可能性があるため、この段階での検証は非常に重要でした。
また、パフォーマンス検証の過程で通信プロトコルの選定も行いました。gRPCのほかにRESTやGraphQLも候補に挙がりましたが、最終的にはgRPCを採用しました。その理由は、以下の通りです:
- パフォーマンス面での優位性
- 既存のGoで実装されたgRPC APIサーバーとの互換性
とくに後者は重要な要因でした。連携方式を大幅に変更することで、移行に時間がかかったり、プロダクト開発チームに大きな負担をかけてしまっては本末転倒だからです。
パフォーマンス検証では、以下を検証しました:
- 現状の各連携方式のパフォーマンス計測
- プロトタイプと既存連携方式の速度比較
- 各通信プロトコル(REST, gRPC, GraphQL)の速度比較
- gRPCフレームワークの選定と速度比較
これらの検証を通じて、大きなパフォーマンス劣化がないことを確認したため、我々は「Tsugite」の本格的な開発に着手し始めることとなりました。
第2章: 推進するための思考と道のり
連携を進めるにあたって、まずはファーストユーザーを決め、そのユーザーが必要とする機能の実装を進めました。1stユーザーは、ちょうど新規プロダクトで開発を進めていたチームがプロダクト間連携を必要としていたため、このチームを選定しました。
最初からそれなりにコールされているAPIを置き換えてしまうとリスクが高いため、新規のプロダクトで単純なAPIから置き換える形で進めました。その際に、ファーストユーザーのチームと連携し、必要なAPIとデータを洗い出して、並走しながら開発を進めました。 また、最初はReadのAPIのみにスコープを絞って提供を開始しました。これは、Goで実装されたgRPC APIサーバーはReadのAPIを提供していたので、まずはReadのみで置き換えは可能であると判断したためです。
またファーストユーザーのチームと議論を重ねて、Protoの記述時のルールの制定などを行いました。Protoの記述時のルールは、後々のメンテナンス性を考慮して、以下を定めました:
- ディレクトリ構成
- 命名規則
- サービスの分割方針
また、「Tsugite」を推進するにあたって、オーナーシップはリアーキテクティングチームが持つことにしましたが、コントリビューションに対しては他のチームにも開放することを意識しました。 これは、最終的には使用するプロダクト開発チーム側が簡単にメンテナンスできて、必要なデータが新しく追加された際にも迅速に対応できるようにするためです。
そのため、APIについてもどのように書いてもらうかを意識して検討を進めていました。 というのも、「Tsugite」はプロダクト間通信のためのgRPC API群でもあるのですが、共通データを取得するためのgRPC APIを作るためのライブラリのようなものを提供するという目的も含まれていると考えたからです。
要は、「Tsugite」は単にAPIを提供しているだけではなく、最初はリアーキテクティングチームが実装を進めているかもしれないが、今後は他のチームが欲しいAPIやデータを追加していく未来があるということです。 その時に、混乱するような作りになっていてはいけない。そして、混乱しないためにはどうすればいいかというと、Railsの理解があれば困らない、詰まらない、迷わない状態であるのが理想です。
Rubyはメタプログラミングを駆使すれば、書いてもらうことは本当に最小限にできる一方で、あまりにもメタプログラミングを駆使しすぎると、他の人が読んだり、メンテナンスするのが難しくなることがあります。 なので、基本的にはRailsライクに書けるようにすることを意識して、整理していきました。
gRPCフレームワークはgrufを採用しているので、grufを使ってコントローラーにバインドして作成しています。また、モデルは共通データを管理しているRailsのモデルをそのまま使っています。 そして、プレゼンター層を用意して、プレゼンター層でコントローラーが取得したデータをレスポンスの形式に整形して返すようにしています。
なので、コントローラーとしては次のような形になっています:
class Tsugite::Common::ClientsController < Tsugite::ApplicationController def get_client client = Client.find_by!(id: request.message.path.id) Tsugite::Common::GetClientResponsePresenter.new(client) end end
そして、プレゼンター層は次のような形になっています:
class Tsugite::Common::GetClientResponsePresenter < Tsugite::ApplicationPresenter def initialize(client) super() @client = client end def client Tsugite::ClientPresenter.new(@client) end end class Tsugite::ClientPresenter < Tsugite::ApplicationPresenter attr_reader :client delegate_missing_to :client def initialize(client) super() @client = client end
そして、各コントローラーが継承しているTsugite::ApplicationController
にてプレゼンターが変換したデータを元にレスポンスを返しています:
def process_action(method_key, &) presenter = super if presenter.is_a?(Tsugite::ApplicationPresenter) fields = presenter.as_json(field_options.options) # FieldMaskを考慮してレスポンスのフィールドを絞る request.response_class.new(**fields) else raise GRPC::Internal, "tsugite's API should return an instance that inherits from `Tsugite::ApplicationPresenter`" end end
また、プロダクト開発チームが「Tsugite」を利用する際に迷わないよう、ドキュメントの充実に力を入れています。 ドキュメントは「開発者向けガイドライン」と「ユーザー向けガイドライン」の2種類を用意しています。
- 開発者向けガイドライン
- Proto定義の方法、APIの作成手順、実装時の注意点に焦点を当て、技術的な詳細を網羅しています。
- ユーザー向けガイドライン
- 開発環境へのデプロイ方法、起動方法、接続先の情報、リリース手順、ログ管理や監視項目など、「Tsugite」を使用する際に必要な情報を提供しています。
また、アンドパッドには日本語話者と非日本語ネイティブのエンジニアが混在しており、リアーキテクティングチームにも非日本語ネイティブのエンジニアが在籍しているため、日本語と英語の両方でドキュメントを用意しています。 英語の文書作成については、リアーキテクティングチームの@kei-sが過去に執筆した記事をご覧いただければ分かるように、「完璧」を目指しすぎず、シンプルで分かりやすいものを心がけています。とはいえ、ドキュメントについては非日本語ネイティブのエンジニアの方にレビューをしてもらって、違和感がないかどうかを確認しています。
さらに、「Tsugite」に対する要望やフィードバックを出しやすい環境を整えるため、GitHub Projectsのタスクボードを活用して要望を受け付けています。
そして日本語の方が相談しやすいという方向けのためにも、日本語での相談もできるようにしています。たとえば、Slackでカジュアルに相談してもらっても良いことにしています。 日本語で受けた依頼や要望は、英語に翻訳して非日本語ネイティブのメンバーに共有し、可能な限りユーザー側がフィードバックや要望を出しやすい環境を整えています。
理想を言えば、すべて英語に統一することでコストを削減することが望ましいのですが、移行プロセスで負担を増やしてしまうことは避けたいと考えています。そのため、現状では日本語と英語の両方に対応する形を採用しています。
導入にあたっては、プロダクト開発チームごとに事前のヒアリングを行い、最適な導入方法を調整しています。たとえば:
- Railsを日常的に使用しているチームの場合、Proto定義やAPIの実装に問題がないため、設計や実装のレビューをサポートする形を採用。
- Goを主に使用しているチームの場合、Protoの設計は可能でもAPIの実装が難しいため、設計レビューに加え、API実装をリアーキテクティングチームが担当。
このようにチームごとの状況に応じて柔軟に対応し、スムーズな導入を進めています。
終章: プロダクト統合の現在地と未来
これまでの取り組みの結果、ファーストユーザーに必要なAPIの提供は完了し、現時点では「Tsugite」を利用したデータ連携が可能になっています。 さらに、他の置き換え対象のプロダクトについてもヒアリングを進めながら現在置き換えが着実に進んでいます。
まだすべてのプロダクトが「Tsugite」に移行できたわけではありませんが、新規プロダクトについては、最初から「Tsugite」を利用して開発を進めることができる状況が整っています。実際に最近では「Tsugite」を用いた新規プロダクトが短期間でリリースされるなど、当初の目的であったプロダクト間連携の円滑化は着実に進展していると言えるでしょう。この進展により、ReadAPIに関しては「Tsugite」に移行するための仕組みの土台がほぼ整ったと考えています。
一方で、WriteAPIの移行については依然として課題が残っています。WriteAPIにはより高い信頼性が求められるため、慎重な移行が求められており、現在はその準備を進めている段階です。「Tsugite」の安定稼働に向けて、さらなる信頼性と可用性の向上を目指し、多方面で取り組みを進めています。
今後、新たなプロダクトの追加や既存プロダクトの拡張に伴い、プロダクト間連携の需要は一層高まることが予想されます。そのため、「Tsugite」の機能拡張や性能向上を継続的に進め、プロダクト間連携をさらに円滑にしていく予定です。
未来への展望として「Tsugite」はまだ進化の途中にありますが、プロダクト間連携をよりスムーズにするための新たな機能や仕組みを提供し、プロダクト開発チームがより効率的に開発を進められる環境を整えていきたいと考えています。 もしこの取り組みに興味を持たれたなら、私たちと一緒に「Tsugite」の未来を形作る旅に参加してみませんか?プロダクト間連携の新たな可能性を切り開く挑戦に、ぜひ加わっていただければと思います。
- リアーキテクティングチームとはアンドパッドのプロダクトの全体を俯瞰して、複数のドメインを横断した問題を解決するチームです。 https://www.wantedly.com/companies/andpad/post_articles/471522↩
- 継手が由来です、パイプやチューブ、ホース同士をつなぎ合わせるものを総称して継手と呼びます。プロダクト間連携をするためのgRPC APIサーバーとして、プロダクト同士をつなぎ合わせるものという意味でこの名前がつけられました。良い名前ですよね。↩
- スクラムにおける厳密な意味でのプロダクトオーナーではないです。プロダクトビジョン、プロダクトバックログ管理、および関係者との調整だけでなく、プロダクトの方向性の意志決定をしていたり、もちろん詳細の設計や開発も行っています。↩