自己紹介

松村有倫氏(以下、松村):それでは、「Goでprotocプラグイン作った話」というタイトルで発表します。

自己紹介します。松村有倫と申します。2020年に新卒でDMM.comに入社して、現在はプラットフォーム事業本部の基盤開発グループで働いています。Go歴は1年くらいで、ここ1年間は、Go製プロダクトの保守だったり、プロキシやツールの実装だったりをGoでやってきました。

本日は、運用自動化のためのツールをGoで自作した話をします。「Protocol Buffers」のスキーマ定義からGoのコードを生成するツールです。

それと関連して、GoでGoのコードを生成する時についても少し話せるとよいかなと思っています。よろしくお願いします。

Protocol Buffers+GitでAPI定義の管理基盤を構築する

まず、僕が所属している基盤開発グループの紹介からしたいと思います。プラットフォーム事業本部(以下、PF)では、各事業共通で使われる会員基盤だったり、決済基盤だったりをWebAPIで提供していて、僕が所属している基盤開発グループは、それらのAPIの横断処理の部分を担当しています。持ち物のプロダクトとしては、Go製のAPI Gatewayや認可APIなどを持っています。

これ以外にも、PF全体のAPI改善に向けた取り組みも行っていて、最近だと、PF内のDDD推進も行っています。

さて、そんな基盤開発グループですが、最近「API-Gatewayが提供するAPI定義の管理基盤が欲しい」という話が上がってきています。

「APIの追加時にレビューをして、APIの品質を高めていきたい」「管理したデータを活用して各種の自動化みたいなのが行えるとよい」といった部分がモチベーションにつながっています。

そこで、このAPI定義の管理基盤をProtocol BuffersとGitで構築できないか、というプロジェクトを最近は進めています。

Google製の「Protocol Buffers」をAPI定義の管理フォーマットとして採用

「Protocol Buffers」について、少し紹介したいと思います。Protocol Buffersは、Googleが作ったプログラミング言語非依存の通信データのシリアライズフォーマットです。

専用のInterface Definition Language(IDL)で通信スキーマを定義して使うかたちになっています。例えばこんな感じで、「Ping」というサービスがあって、その中にPingというrpcメソッドがあります。リクエストの型がPingReqで、中身の定義は(スライドを示して)こんな感じ。レスポンスはPingRespという型を返して中身は(スライドを示して)こんな感じ。という風に専用のDSLで通信スキーマを定義して使えるものになっています。

今回、API定義の管理フォーマットとしてProtocol Buffersを採用した理由ですが、まず、スキーマ記述言語としてIDLが非常に使いやすかったことが挙げられます。

Protocol BuffersのIDLは部分構造を分けて記述できますし、それ以外でもいろいろと簡素で読みやすいものになっています。あとは、「protoc」というProtocol Buffersのコンパイラによって定義した「スキーマ定義」を幅広く活用できるのも利点の1つだと思います。

すでにスキーマ定義からgRPCを実装したり、ドキュメントを生成したりするツールが世の中にありますし、自分のプラグインで拡張すると、ほかにもさまざまなコード生成が可能になります。

このあたりの特徴から、API定義のフォーマットとして、Protocol Buffersは非常に優秀だと思って、API定義の管理フォーマットとしてProtocol Buffersを採用しました。

今日は「プラグインで拡張すれば、ほかにもさまざまなコード生成が可能」という部分を紹介したいと思います。

API-Gateway用のREST/gRPC変換プロキシ「rest-frontend」のコード作成

例として、「rest-frontend」というAPI-Gateway用のREST/gRPC変換プロキシの話をします。

これが必要になった経緯ですが、API-GatewayとしてRESTのバックエンドとgRPCのバックエンドの両系統をサポートしつつ、一方でクライアントには、共通してRESTで提供する構成を取りたいという話が出てきました。

その過程で、gRPCのバックエンドに対して、RESTとgRPCの変換を行うモジュールとして、API Gatewayの後ろに、rest-frontendという変換プロキシを置くことになりました。

このrest-frontendのコードは、実装に使うコード自体もProtocol Buffersからツールで生成しています。「grpc-gateway」というツールがあって、protobufからサービス定義ごとに、RESTとgRPCの変換を行うプロキシのGo実装を生成してくれます。

コードは、こんな感じになっています。まず、NewServeMuxという関数で、gRPC用のGateway(grpc-gateway用のルータ)の生成を行います。その生成したルーターに対して、ホストを指定してサービスごとにプロキシをマウントしていきます。

例えば、生成のもとになったprotoファイルに「foo」というサービスと「bar」というサービスがある場合、RegisterFooHandlerFromEndpointで、fooのプロキシをマウントして、バックエンドのホストはfoo.example.com。

あとは、barに対して、RegisterBarHandlerFromEndpointという関数でプロキシをマウントして、ホストはbar.example.comみたいに指定します。このように、サービスごとにプロキシをマウントして、最後にhttp.ListenAndServeで、通常のhttp.Handlerとしてlistenすることで、変換プロキシを実装できます。

protocプラグイン制作のモチベーション

(スライドを示して)ここですが、grpc-gatewayはそれぞれのサービスごとにgRPCの変換部分のコードを自動生成してくれますが、最後にホストを指定して、ルーターに埋め込む部分は手作業になってしまいます。

このルーティング設定をなんとか自動化できないかと考えました。ホストの設定は、コードに直接埋め込むのではなく、別に管理して注入できるようにしたいという話があったからです。

また、「Gatewayに新規のサービスが追加されるたびに、いちいち作業するのはちょっと面倒くさい」というのもモチベーションだったりします。

今回はprotoファイルの中にサービスの一覧の定義があるので、このルーティング設定の部分も、protocプラグインで自動生成できるのではないかと考えて、プラグインを制作する流れになりました。

protocのプラグインの実装

ここからは、protocのプラグインの実装について、紹介していきたいと思います。

protocのプラグインの実体ですが、protocと標準入出力で通信する実行形式になっています。この通信フォーマットが、なんとprotobufになっていて、protobufのツールであるprotoc自身にprotobufが使われているという、ちょっとかっこいい作りになっています。

実はプラグインの実装に使える言語は、protobufを解釈できて、実行形式をビルドできるものであれば自由になんでも使えるのですが、今回はGoで実装することにしました。

作りとしては、protocから標準入力経由で、protobuf形式のprotoファイル情報をもらってきます。

それとは別に、プラグインにバックエンドのホスト設定をYAMLの形式で渡して、この2つから情報を抽出して、出力ファイルの情報もprotobuf形式で標準出力に返します。すると、protocがそのレスポンスを標準出力から読み取って、ルーターのコードを生成してくれる流れになります。

3つのパッケージを利用

使うライブラリですが、Go言語の場合は、google.golang.orgの「protbuf」というモジュールに全部まとまっています。

主に利用するパッケージは3つ。1つ目は「proto」っていうパッケージです。これはprotobufの各種処理をGoから行うためのライブラリです。

2つ目が「types/pluginpb」。これは、protocとプラグインの間の通信に対するメッセージの型を管理するためのパッケージです。

最後が「types/descriptorpb」。protocから渡されてくるprotoファイルの情報を記述するためのメッセージ型を管理するための定義が入ったパッケージです。

(次回へつづく)