default image

この記事は、Merpay Tech Openness Month 2020 の 8 日目の記事です。

こんにちは。メルペイのコード決済チームでバックエンドエンジニアをしている @ktr です。

メルペイではマイクロサービスアーキテクチャを採用しており、それぞれのマイクロサービス間の通信プロトコルとして gRPC を、リクエスト・レスポンスのシリアライズフォーマットとして Protocol Buffers を採用しています。gRPC + Protocol Buffers を採用することにより、API のクライアントとサーバのインターフェース部分のコードを自動生成でき、リクエスト・レスポンスのエンコード、通信処理といった gRPC 特有の処理をほとんど意識せずに利用できます。

しかし、その一方で Protocol Buffers はバイナリベースのシリアライザであるため、例えば一般的な HTTP ベースの REST API のようにリクエストの手動で組み立てや、レスポンスの目視でのチェックは困難です。そのため、ユーザはなんらかの gRPC クライアントツールを利用して gRPC サーバを検証することになります。

この記事では私が開発している Evans が持っている機能や、それらの機能を開発する際に必要となった gRPC 周辺技術を交えつつ紹介します。なお、この記事では Evans のバージョンは 0.9.0 を前提としています。

さまざまな gRPC クライアントツールと Evans

gRPC クライアントツールの数は多くありませんが複数存在しています。ここではコマンドラインツールのみに絞って著名なものをいくつか紹介します。

grpc_cli

gRPC の公式リポジトリに同梱されている grpc_cli は公式の gRPC クライアントツールといえますが、最低限の機能しか備えていません。例えば他の gRPC クライアントツールではほぼ実装されているメタデータの送信ができない、JSON 形式でのリクエスト内容の記述を受け付けられないといった問題があります。また、インストールするためにはソースコードからビルドする必要があり煩雑であるため、あまりメジャーには使われていません。 ただし、サードパーティのツールは grpc_cli のインターフェースをベースに設計されているものが多くあるため、ツールをつくるときは参考になるでしょう。

fullstorydev/grpcurl (gRPCurl)

gRPCurl は現在最も使われている gRPC クライアントツールです。2017 年頃から存在し、現在も活発にメンテナンスされています。機能面でもたいていのユースケースは網羅されており、機能の不足で困るようなことはほとんどないでしょう。また、作者の @jhump 氏は後述する Protocol Buffers リフレクションの Go 実装のライブラリも作成・メンテナンスしています。

uber/prototool (Prototool)

Prototool は Uber Technologies によって開発された Protocol Buffers のユーティリティツールです。Prototool には gRPC のエンドポイントを呼び出せるサブコマンドが付属しています。ただし、このサブコマンドは fullstorydev/grpcurl に大きく依存しており、実質 gRPCurl のサブセットとなっています。また、Prototool は開発が停滞しており、現在は Protocol Buffers のユーティリティツールとして Buf を推奨する旨がアナウンスされており、Buf は gRPC クライアントツールとして gRPCurl を 推奨 しているため、Prototool を使うメリットはなくなってきています。

ktr0731/evans (Evans)

Evans は今回紹介する拙作の gRPC クライアントツールです。gRPCurl と同じく 2017 年より開発を行っています。他の gRPC クライアントツールは非インタラクティブな UI だけを提供するのに対し、Evans は非インタラクティブな UI である CLI モードとインタラクティブな UI である REPL モードの二つを提供しています。インタラクティブな機能を提供している gRPC クライアントツールとしては最も使われています。

以降では CLI モードと REPL モードがどのように実装されているかを紹介します。

CLI モード

CLI モードは grpc_cli や gRPCurl と同じような非インタラクティブな機能を提供するモードです。 gRPCurl を始めとする大抵の gRPC クライアントツールと同じように JSON 形式でリクエストを送信できたり、gRPC サーバが公開しているサービスや、メソッド、メソッドのリクエスト・レスポンス型などをコマンドラインから取得できます。また、出力を JSON 形式に変えることができるため、他のコマンドと組み合わせて使う際にも役に立ちます。

例えば、以下のようにして Evans を CLI モードで使用できます。ここで、gRPC サーバは api.proto という定義を元に実装されているものとします。

$ evans --proto api.proto cli list # サービスの列挙
api.Example

$ evans --proto api.proto cli list api.Example # メソッドの列挙
api.Example.Unary

$ evans --proto api.proto cli desc api.Example.Unary # Unary というメソッドの定義を表示
api.Example.Unary:
rpc Unary ( .api.Request ) returns ( .api.Response );

$ evans --proto api.proto cli desc api.Request # Request というメッセージの定義の表示
api.Request:
message Request {
  string name = 1;
}

$ echo '{ "name": "ktr" }' | evans --proto api.proto cli call api.Example.Unary # Unary の呼び出し
{
  "message": "hello, ktr"
}

gRPC リフレクション

先程の例では、--proto フラグを使い、サーバの定義元となる Protocol Buffers ファイルを指定していました。しかし、システムが大きくなったり、マイクロサービスのように複数のシステムに分割されたりしていくと依存するファイルが複数になり、指定が面倒になっていきます。

gRPC にはリフレクションが定義されており、サーバがリフレクションを有効化することによりファイルを指定せずに情報が取得できるようになります。 リフレクションを利用すると先程の例を以下のように書き換えられます。--reflection (省略形は -r) により、統一的な方法でそれぞれの情報にアクセス・メソッドの呼び出しができるようになりました。

$ evans -r cli list # サービスの列挙
api.Example

$ evans -r cli list api.Example # メソッドの列挙
api.Example.Unary

$ evans -r cli desc api.Example.Unary # Unary というメソッドの定義を表示
api.Example.Unary:
rpc Unary ( .api.Request ) returns ( .api.Response );

$ evans -r cli desc api.Request # Request というメッセージの定義の表示
api.Request:
message Request {
  string name = 1;
}

$ echo '{ "name": "ktr" }' | evans -r cli call api.Example.Unary # Unary の呼び出し
{
  "message": "hello, ktr"
}

リフレクション自体も Protocol Buffers を使用して 定義 されています。クライアントは通常のサービスのメソッドを呼び出す時と同様にリフレクションサービスのメソッドを呼び出し、必要な情報を取得します。

REPL モード

REPL モードは Evans の最も特徴的な機能です。REPL モードは「できる限り楽に手動テストを行う」ことを目的にして作っています。

普段の開発では自動化されたテストが存在しているのが当たり前であるため、頻繁に手動テストをすることはそう多くありません。そのため、手動テストの際にサービス名やリクエスト・レスポンスの型、enum の種類などを失念していることも多々あり、gRPC クライアントツールからメソッドを呼び出すために都度 Protocol Buffers 定義を確認しなければいけません。

REPL モードでは定義を完全に覚えていなくても強力な補完機能によって入力をサポートしてくれるため、迷いなくリクエストできます。

repl サブコマンドを実行すると以下のように REPL が起動し、インタラクティブにサーバとやりとりできます。Evans は gRPC リフレクションを使用してサーバが持つ情報を取得し、補完によってそれらの情報をユーザに伝えます。メソッドの呼び出しをする call コマンドを実行すると、リクエストの入力用のプロンプトが現れ、フィールドを順番に入力できます。入力が完了するとリクエストが実行され、レスポンスが JSON 形式で表示されます。

Protocol Buffers リフレクション

gRPC ベースのシステムを構築する場合、Protocol Buffers で記述したインターフェース定義を元にリクエスト・レスポンスの型を自動生成して利用することが一般的ですが、gRPC クライアントツールの場合、汎用的なツールであるため特定の定義から自動生成される型を保持していません。

そのため、これらのツールは Protocol Buffers のリフレクション機能を利用してリクエストやレスポンスを Protocol Buffers で定義された型としてエンコード・デコードしています。

Protocol Buffers にはディスクリプタ (descriptor: 記述子) という要素があります。ディスクリプタは Protocol Buffers の定義ファイルからコンパイルされ、ファイルやサービス、メソッド、メッセージなど、それぞれの要素に対応したディスクリプタが生成されます。それぞれのディスクリプタもまた Protocol Buffers を使って 定義 されています。

ディスクリプタには Protocol Buffers の型としてエンコード・デコードするために必要なメタデータが含まれているため、これを利用することで自動生成された型がなくとも動的にエンコード・デコードができるようになります。

ディスクリプタは Protocol Buffers 定義ファイルもしくは gRPC リフレクションを介して取得できます。

jhump/protoreflect

この記事の最初に紹介したように、gRPCurl の作者である @jhump 氏は Protocol Buffers リフレクションの Go 実装である jhump/protoreflect の作者でもあります。このライブラリが公開されてからしばらくの間、Protocol Buffers の Go 実装でリフレクションが提供されていなかったため、Go で Protocol Buffers リフレクションを利用できる唯一のライブラリでした。 Evans も Go で実装されているため、protoreflect には最初からお世話になっています。

動的にメッセージを構築するためのパッケージ dynamicdynamic.Messageproto.Messageproto.Marshaler を実装する型であり、フィールドに MessageDescriptor と、メッセージのフィールドナンバー、フィールド値を管理するためのマップを保持しています。メッセージをエンコードするためには proto.Message を用いますが、この関数は内部でメッセージが proto.Marshaler を実装しているかを型アサーションでチェックし、実装している場合はそれを呼び出します。dynamic.MessageMarshal メソッドは自身が持つ MessageDescriptor とマップを使うことで動的にエンコードし、その結果を返します。

最近では Go の Protocol Buffers ライブラリの v2 が登場し、同時にリフレクションも公式にサポートされるようになりました。しかし、細部こそ違いはあるものの、protoreflect のインターフェースと非常に類似しており、protoreflect が Go の Protocol Buffers エコシステムにおいて大きな貢献をしてきたことが感じられます。

REPL モードの TUI

REPL モードでは入力を待ちうけるプロンプトや、入力の補完の表示など、グラフィカルなインターフェースを備えています。このインターフェースは主に c-bata/go-prompt を用いて実装されています。 go-prompt は myclipgcli が使っている python-prompt-toolkit の Go 実装で、このライブラリを使用することで複数のプラットフォームの差異や画面の描画処理等を意識せずに強力な TUI を構築できます。

go-prompt は補完機能も備えており、Evans では gRPC リフレクションや Protocol Buffers リフレクションによって得た gRPC サービス、メソッドの名前や、メッセージが持つフィールドの情報などを入力に合わせて補完しています。

Evans の配布

Evans は Go 実装であるため、go get コマンドによってインストールすることも可能ですが、gRPC クライアントツールのユーザは Go を使っているエンジニアだけとは限らないため、どのような人・どのようなプラットフォームであっても楽にインストールできるような仕組みを整えています。

リリースの配布は Homebrew や GitHub Releases によって行っています。macOS ユーザの場合は主に Homebrew でインストールすることを想定しており、それ以外のプラットフォームでは GitHub Releases に用意されているバイナリを実行パスにインストールすることを想定しています。

各プラットフォーム向けのビルドや Homebrew Tap の更新、GitHub Releases へのアップロードは GoReleaser を使用しています。設定ファイルを用意し、コマンドを実行するだけでリリースに必要な一連の操作をすべて行ってくれるため非常に強力なツールです。

Evans のアップデート

コマンドラインツールにはアップデートを行うための標準的な仕組みが存在しません。そのため、開発者はツールの新しいバージョンを配布しづらく、ユーザはツールのアップデートを知ることが難しくなっています。

Evans ではそれらの問題を解決するためにツールのアップデートを行うためのライブラリを用意しており、新しいバージョンが配布されるとユーザに通知され、アップデートを選択すると自動的にバイナリが更新されるようになっています。

おわりに

この記事では拙作の gRPC クライアントツール Evans の機能や仕組み、gRPC 周辺技術を紹介しました。 Evans は gRPC や Protocol Buffers が提供する機能や、さまざまな OSS の恩恵を受けて作られているツールであり、私一人では作れなかったと思います。 この記事を通して Evans や gRPC 周辺技術に興味を持っていただければ幸いです。

関連記事