GitBook 上で動作しています
実践: gRPC in Ruby
本章では、gRPC in Ruby の Quick Start から一歩先、「Wantedly で Ruby を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。

Ruby で gRPC Server を作る

ディレクトリ構成 with Rails

まずはディレクトリ構成について説明します。基本的に Ruby on Rails の上で作ることを想定しています。
1
.
2
├── app
3
│ ├── grpc_services
4
│ ├── models
5
│ └── pb_serializers
6
├── bin
7
├── config
8
└── db
Copied!
特徴的な部分を紹介します。
    app/grpc_services
      protobuf から生成したサービスクラスを継承したサービスクラスを置きます
      ファイル名およびクラス名にはそれぞれ _grpc_service, GrpcService という suffix を付与します
    app/pb_serializers
      Protobuf Message object を生成する pb-serializer の実装クラスを配置します
      ファイル名およびクラス名にはそれぞれ _pb_serializer, PbSerializer という suffix を付与します

wantedly/apis - gRPC のインタフェース定義

apis は Wantedly で .proto file を集約するための repository です。apis へ PR を作るだけで、CI によって「各言語向けのコード生成」が自動で行われるようになっており、「protoc のセットアップ」などの煩雑な作業を個々の開発者がしなくても良い仕組みになっています。
Ruby であれば wantedly/apis-ruby というリポジトリに生成コードがコミットされ、private gem として配信されるようになっています。 各マイクロサービスリポジトリでは、Gemfile に apis を記述して bundle install を行い、 必要な file を require して利用するというフローになっています。
インタフェース定義におけるルール・tips などはprotobufスキーマとgRPC通信を参照してください。

RPC の実装

さて、apis repository によって「gRPC Service Class」と「Protocol Buffers Message Class」のコードを生成して配信することができました。gRPC Server が意味のある response を返すには、これらを利用して ロジックを実装する必要があります。
一例として、ProfileService の BatchGetProfiles という RPC の処理を見てみましょう。 以下のように、apis で生成された gRPC Service Class である W::UsersPb::ProfileService::Service を継承した ProfileGrpcService Class で、 batch_get_profiles method を実装します。 この例は簡略化して書いてますが、実際にはメソッド内でDB への問い合わせるなどして意味のあるデータを保持したオブジェクトを作成、 最終的に W::UsersPb::BatchGetProfilesResponse オブジェクトとして返します。
1
# wantedly/apis で .proto から生成した Ruby の実装を読み込む
2
# `foo.proto` というファイルから `foo_pb.rb` と `foo_services_pb.rb` という2つのファイルが生成される
3
# gRPC に関するコードが記述されてるのは `_services_pb` の方なので、そちらを require する
4
require 'wantedly/users/profile_services_pb'
5
​
6
# Protobuf で `wantedly.foo.bar` というパッケージがあったとき、
7
# Ruby では `W::Foo::BarPb` というモジュールとする規約になっている
8
class ProfileGrpcService < W::UsersPb::ProfileService::Service
9
# @param [W::UsersPb::BatchGetProfilesRequest] req
10
# @param [GRPC::ActiveCall::SingleReqView] call
11
# @return [W::UsersPb::BatchGetProfilesResponse]
12
def batch_get_profiles(req, call)
13
# ...
14
​
15
W::UsersPb::BatchGetProfileResponse.new(profiles: profiles)
16
end
17
end
Copied!
ここで引数に渡ってくる req はリクエストメッセージで、 call は呼び出しに関する情報を持つ GPRC::ActiveCallのオブジェクトです (正確には GRPC::ActiveCall::SingleReqView または GRPC::ActiveCall::MultieReqViewのどちらか)。call.matadata とするとメタデータが取得することができます。

the_pb と pb-serializer によるメッセージの組み立て

Ruby では通常は以下のようにして Protobuf object を作ります。
1
message = W::UsersPb::Profile.new(
2
user_id: profile.user_id, # uint64
3
name: profile.name, # string
4
avatar_url: profile.avatar_url, # string
5
)
6
message.to_proto # => binary
Copied!
上記は最も単純な例です。protobufスキーマとgRPC通信で紹介されているラッパー型など、 いわゆる Well-Known Types が入ってくると、もっと面倒なコードを書くことになります(面倒なのでここには書きません)。
ここで活躍するのが the_pb と pb-serializer という2つの gem です。
the_pb は Ruby オブジェクトを前述した Well-Known Types に変換するユーティリティを提供する gem です。 pb-serializer は ActiveModelSerializers のような DSL で Protobuf object の Serializer を実装できる gem です。内部では the_pb に依存しています。
先程例に出した Profile object の serializer であれば以下のように実装できます。
1
require 'wantedly/users/profile_pb'
2
​
3
# @attr_reader [::Profile] object
4
class ProfilePbSerializer < Pb::Serializer::Base
5
message W::UsersPb::Profile
6
​
7
attribute :user_id
8
attribute :name
9
# avatar_url が google.protobuf.StringValue だったとしても、よしなに変換してくれる
10
attribute :avatar_url, allow_nil: true
11
end
12
​
13
ProfilePbSerializer.new(profile).to_pb
14
# => <W::UsersPb::Profile ...>
Copied!
the_pb と pb-serializer をうまく活用することで、gRPC server 実装時の手間を大きく削減することが出来ます。 pb-serializer の詳しい使い方についてはリポジトリ の README や examples を参照してください。

エラーハンドリング

Ruby における gRPC server の実装では、GRPC::BadStatus というエラークラスを raise することでクライアントにエラーを返すことになります。 また、エラーコードごとに GRPC::BadStatus のサブクラスが用意されており、それを利用することも出来ます。 以下2行は同じ意味になります。
1
raise GRPC::BadStatus, Google::Rpc::Code::NOT_FOUND, "user not found"
2
raise GRPC::NotFound, "user not found"
Copied!
この「エラーコード」は google.rpc.Code に定義されている Protobuf の Enum になります。 エラーコードの使い分けなどについてはprotobufスキーマとgRPC通信を参照してください。

複雑なエラー表現

さて、GRPC::BadStatus と Google::Rpc::Code を利用することでメッセージとエラーコードからなる単純なエラーを表現することは出来ました。 しかし、これでは実際のアプリケーションで表現力が足りません。 例えば単純なフォームのバリデーションエラーでも、フィールドごとにエラーメッセージを保持できる必要があります。
gRPC におけるエラーは実際には google.rpc.Status に定義されるような Protobuf message で表されます。
1
message Status {
2
// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
3
int32 code = 1;
4
​
5
// A developer-facing error message, which should be in English. Any
6
// user-facing error message should be localized and sent in the
7
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
8
string message = 2;
9
​
10
// A list of messages that carry the error details. There is a common set of
11
// message types for APIs to use.
12
repeated google.protobuf.Any details = 3;
13
}
Copied!
この google.rpc.Status のうち、 Any の配列である details にエラーの詳細を詰めることが可能です。 Google API Design Guide の Errors では google/rpc/error_details.proto に定義されるメッセージの利用が推奨されており、Wantedly でもそれに則っています。
Ruby でのエラー詳細の扱いに関しては、the_pb gem で .new_rpc_error という関数が提供されており、それを使うと便利です。
1
raise Pb.new_rpc_error(
2
"post is invalid",
3
code: Google::Rpc::Code::INVALID_ARGUMENT,
4
details: [
5
Google::Rpc::BadRequest.new(
6
field_violations: [{ field: 'title', description: 'タイトルは必須です' }],
7
)
8
],
9
)
Copied!

テスト

gRPC の RPC の実装は見ての通り普通のメソッドなので、最も単純には普通にメソッド呼び出しをして、返り値に対してテストをすることが出来ます。
1
require 'rails_helper'
2
​
3
RSpec.describe ProfileGrpcService do
4
let(:grpc_service) { described_class.new }
5
let(:call) { instance_double("GRPC::ActiveCall::SingleReqView", metadata: {}) }
6
​
7
describe '#batch_get_profiles' do
8
let(:request) { W::UsersPb::BatchGetProfilesRequest.new(user_ids: user_ids) }
9
let(:response) { grpc_service.batch_get_profiles(request, call) }
10
let(:user_ids) do
11
# ...
12
end
13
​
14
it 'returns user profiles' do
15
# ...
16
end
17
end
18
end
Copied!
ただ、これだと Protobuf の marshal / unmarshal 含め gRPC の内部実装を全く介さないため、 もう少し丁寧にやるなら GrpcTesting module (private library) の利用を検討するといいでしょう。
1
require 'rails_helper'
2
​
3
RSpec.describe ProfileGrpcService do
4
include GrpcTesting
5
let(:stub) { test_stub_class(described_class.superclass.module_parent).new(described_class.new) }
6
​
7
describe '#batch_get_profiles' do
8
let(:response) { stub.batch_get_profiles(request, call) }
9
​
10
# ...
11
end
12
end
Copied!

起動・デバッグ

次に、gRPC Server Process を起動します。gRPC Server を起動して Request を処理させるには、app/grpc_services に定義した「gRPC Service Class を継承した Class」を Handler として登録する必要があります。また、Wantedly では、マイクロサービスは Kubernetes 上で Docker Container として動かしているので、Heath Check の為に gRPC Health Checking Protocol を実装した Handler の登録も必要です。その他、Observability を高めるための gRPC interceptor(gRPC の拡張機能。後述するが、Rack Middleware 相当のもの)の設定なども行う必要があります。
こういった「どの repository でも共通で必要になる処理」については、個々の開発者が気にする必要がないよう、servicex (internal) と呼ばれる社内 library (gem) の機能として提供するようにしています。具体的には、grpc-server という名前のコマンドを用意しており、それを実行するだけで「gRPC Service Class の load および Handler としての登録、その他に共通で必要になる各種設定」が行われるようにしています。
grpc-server コマンドは以下のように利用することができます。
1
$ bundle exec grpc-server
2
handling /grpc.health.v1.Health/Check with #<Method: Grpc::Health::Checker#check>
3
handling /grpc.health.v1.Health/Watch with #<Method: Grpc::Health::Checker(Grpc::Health::V1::Health::Service)#watch>
4
handling /wantedly.users.UserService/ListUsers with #<Method: UsersGrpcService#list_users>
5
.
6
.
7
.
8
gRPC server starting...
9
* Listening on tcp://0.0.0.0:6046
10
* Environment: development
11
Use Ctrl-C to stop
Copied!
この grpc-server コマンドのうち、Health Checker やシグナルハンドリングなどの設定を隠蔽したものは grpc_server gem として公開されています。

gRPC server にリクエストを送ってデバッグする

JSON/HTTP な API であれば cURL コマンドや Insomnia, Postman といったツールで実際にリクエストを送ってデバッグすることがあるかもしれません。 gRPC server では標準でコマンドラインツールが提供されている ほか、grpcurl や evans といった 3rd party のツールが存在します。 一方でこれらのツールでは「元になった .proto ファイル」もしくは「Server Reflection に対応した gRPC server」が必要になり、Wantedly の環境では少し使いづらいという問題がありました。
そこで Wantedly では giro というツールを利用し、なるべく少ない手順・依存で gRPC server にリクエストを送れるような環境になっています。詳しくは giro の解説をしているブログ もしくは giro の使い方ドキュメント (internal) を参照してください。

Ruby で gRPC Client を使う

gRPC Client を開発する際にも、gRPC Server 開発と同様に .proto file を集約する apis repository や servicex と呼ばれる社内 library (gem) を活用するようにしています。
gRPC Client としては、apis repository で自動生成した gRPC Service Class のコードを load したうえで、その Stub Class を利用します。gRPC Client を単純に利用するだけであれば、以下のコードで動きます。
1
require 'wantedly/users/profile_services_pb'
2
​
3
client = W::UsersPb::ProfileService::Stub.new(url, :this_channel_is_insecure)
Copied!
ただし、Production の Microservices で利用する上では「必ずセットして欲しい gRPC interceptor(gRPC の拡張機能。後述するが、Faraday Middleware 相当のもの)」などが存在するので、個々の開発者が意識しなくてもそれらの設定が自動で行われる様に、servicex gem の中で .stub_for という「gRPC Client 生成用のメソッド」を用意しています。
1
require 'wantedly/users/profile_services_pb'
2
​
3
client = Servicex::Grpc.stub_for(W::UsersPb::ProfileService, grpc_url)
Copied!
.stub_for の実装はだいたい以下のようになっています。 元々、HTTP/1.1 の通信では servicex gem の中で Faraday Middleware などの設定を行なった API Client を提供する様にしていました。 .stub_for は同様の体験を gRPC でも提供することが意図されています。 Observability を確保するための interceptor のセットや User Agent の設定なども .stub_for で自動で行われるようになっています。
1
# @example
2
# Servicex::Grpc.stub_for(W::UsersPb::ProfileService, UsersApi.base_url)
3
# @param service_class [Class<Grpc::GeneralService>]
4
# @param url [String]
5
# @yield [opts] To modify options for instantiating a Stub
6
# @yieldparam opts [Hash] Default options
7
def stub_for(service_class, url)
8
opts = {
9
channel_args: {
10
'grpc.primary_user_agent' => Servicex.user_agent,
11
},
12
interceptors: Servicex::Grpc.client_interceptors,
13
}
14
yield opts if block_given?
15
service_class::Stub.new(url, :this_channel_is_insecure, opts)
16
end
Copied!
gRPC Client については、基本的にやる事はこれだけです。 「apis と servicex を gem として load すれば、どのマイクロサービスからでも簡単に必要な設定が行われた状態で gRPC での通信が出来る」という環境を作っています。
最終的にはだいたい以下のようなコードを書くことになります。
1
require 'wantedly/users/profile_services_pb'
2
​
3
client = Servicex::Grpc.stub_for(
4
W::Users::ProfileService,
5
UsersGrpcApi.base_url, # servicex が提供する、ほかのマイクロサービスの URL を取得する関数
6
current_user_id: current_user.id,
7
)
8
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)
9
​
10
resp = client.batch_get_profiles(req)
11
resp # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
Copied!

gRPC Client のモック

多くの場合、テストコード中ではマイクロサービス間通信を含む外部へのリクエストを禁止したくなります。 HTTP であれば webmock gem を使うことが多いですが、gRPC では grpc_mock という gem を利用しています。
1
before do
2
GrpcMock.stub_request('/wantedly.users.ProfileService/BatchGetProfiles').to_return do |req, call|
3
W::UsersPb::BatchGetProfilesResponse.new(
4
profiles: [],
5
)
6
end
7
end
Copied!

ライブラリ・ツールの紹介

Ruby で gRPC を利用するときに利用する内製の gem やツールの一覧です。 pb-serializer や giro のように直接的に利用するものもあれば、servicex 内で利用されているものもあります。

話を聞きに行きたい

もっと知りたい

最終更新 29d ago