> For the complete documentation index, see [llms.txt](https://docs.wantedly.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.wantedly.dev/fields/the-system/grpc-in-ruby.md).

# 実践: gRPC in Ruby

本章では、[gRPC in Ruby の Quick Start](https://www.grpc.io/docs/quickstart/ruby/) から一歩先、「ウォンテッドリーで Ruby を使って gRPC Server/Client の開発をどう行なっているのか」について紹介します。

## Ruby で gRPC Server を作る

### Rails としてのディレクトリ構成

まずはディレクトリ構成について説明します。基本的に Ruby on Rails の上で作ることを想定しています。

```
.
├── app
│   ├── grpc_services
│   ├── models
│   └── pb_serializers
├── bin
├── config
└── db
```

特徴的な部分を紹介します。

* `app/grpc_services`
  * protobuf から生成したサービスクラスを継承したサービスクラスを置きます
  * ファイル名およびクラス名にはそれぞれ `_grpc_service`, `GrpcService` という suffix を付与します
* `app/pb_serializers`
  * Protobuf Message object を生成する pb-serializer の実装クラスを配置します
  * ファイル名およびクラス名にはそれぞれ `_pb_serializer`, `PbSerializer` という suffix を付与します

### Rails としてのアーキテクチャ

このセクションは主に Rails 経験者のための解説です。先ほどのディレクトリ構成の背後にあるアーキテクチャと、デフォルトの Rails との差分について軽く触れておきます。図を見てもらいつつ、二段階に分けて説明します。

![](/files/XACOk1sCpbYjy1m8Z75E)

まず、デフォルトの Rails の MVCアーキテクチャ（①）に対して、JSON / HTTP を話す API サーバーにするために ActionView を取り去って ActiveModelSerializers などのシリアライズ機構を導入します。

次に、②のアーキテクチャに対して、Protobuf / gRPC を話せるように各責務に対応するコンポーネントを差し替えたのがウォンテッドリーにおける gRPC サーバーの推奨のアーキテクチャ（③）です。

一見大きく変わったように見えるかもしれませんが、モデルやシリアラズなどの概念は同じなので案外馴染みやすいのではないかと思います（付け加えると、ここで書いていないコンポーネント、たとえば ActiveJob や ActionMailer などについては特に変わることはありません）。どちらかというと実際の開発体験としては、要素技術の変更よりは API のスキーマ情報を先に更新して gem を取り込むというワークフローが入ることが大きな変化になると思います。

ということで、gRPC のインターフェース定義から具体的に説明していきます。

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

apis はウォンテッドリーで .proto file を集約するための repository です。apis へ PR を作るだけで、CI によって「各言語向けのコード生成」が自動で行われるようになっており、「protoc のセットアップ」などの煩雑な作業を個々の開発者がしなくても良い仕組みになっています。

Ruby であれば wantedly/apis-ruby というリポジトリに生成コードがコミットされ、private gem として配信されるようになっています。 各マイクロサービスリポジトリでは、Gemfile に apis を記述した後に bundle install を行って利用するというフローになっています。 以前は apis 内のコードは明示的に require を書かなければ読み込まれなかったのですが、バージョン 2021.12.22.1128 以降から自動的に読み込まれるようになったので、現在では個々の開発者が require を書く必要はありません。

インターフェース定義におけるルール・tips などは[protobufスキーマとgRPC通信](/fields/the-system/apis.md)を参照してください。

### 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` オブジェクトとして返します。

```ruby
# Protobuf で `wantedly.foo.bar` というパッケージがあったとき、
# Ruby では `W::Foo::BarPb` というモジュールとする規約になっている
class ProfileGrpcService < W::UsersPb::ProfileService::Service
  # @param [W::UsersPb::BatchGetProfilesRequest] req
  # @param [GRPC::ActiveCall::SingleReqView] call
  # @return [W::UsersPb::BatchGetProfilesResponse]
  def batch_get_profiles(req, call)
    # ...

    W::UsersPb::BatchGetProfileResponse.new(profiles: profiles)
  end
end
```

ここで引数に渡ってくる `req` はリクエストメッセージで、 `call` は呼び出しに関する情報を持つ `GPRC::ActiveCall`のオブジェクトです (正確には `GRPC::ActiveCall::SingleReqView` または `GRPC::ActiveCall::MultiReqView`のどちらか)。`call.metadata` とするとメタデータが取得することができます。

### the\_pb と pb-serializer によるメッセージの組み立て

Ruby では通常は以下のようにして Protobuf object を作ります。

```ruby
message = W::UsersPb::Profile.new(
  user_id: profile.user_id,        # uint64
  name: profile.name,              # string
  avatar_url: profile.avatar_url,  # string
)
message.to_proto  # => binary
```

上記は最も単純な例です。[protobufスキーマとgRPC通信](/fields/the-system/apis.md)で紹介されているラッパー型など、 いわゆる [Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) が入ってくると、もっと面倒なコードを書くことになります（面倒なのでここには書きません）。

ここで活躍するのが [the\_pb](https://github.com/wantedly/the_pb) と [pb-serializer](https://github.com/wantedly/pb-serializer) という2つの gem です。

**the\_pb** は Ruby オブジェクトを前述した Well-Known Types に変換するユーティリティを提供する gem です。 **pb-serializer** は [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) のような DSL で Protobuf object の Serializer を実装できる gem です。内部では the\_pb に依存しています。

先程例に出した `Profile` object の serializer であれば以下のように実装できます。

```ruby
# @attr_reader [::Profile] object
class ProfilePbSerializer < Pb::Serializer::Base
  message W::UsersPb::Profile

  attribute :user_id
  attribute :name
  # avatar_url が google.protobuf.StringValue だったとしても、よしなに変換してくれる
  attribute :avatar_url, allow_nil: true
end

ProfilePbSerializer.new(profile).to_pb
#  => <W::UsersPb::Profile ...>
```

the\_pb と pb-serializer をうまく活用することで、gRPC server 実装時の手間を大きく削減することが出来ます。 pb-serializer の詳しい使い方についてはリポジトリ の README や examples を参照してください。

### エラーハンドリング

Ruby における gRPC server の実装では、`GRPC::BadStatus` というエラークラスを raise することでクライアントにエラーを返すことになります。 また、エラーコードごとに `GRPC::BadStatus` のサブクラスが用意されており、それを利用することも出来ます。 以下2行は同じ意味になります。

```ruby
raise GRPC::BadStatus, Google::Rpc::Code::NOT_FOUND, "user not found"
raise GRPC::NotFound, "user not found"
```

この「エラーコード」は `google.rpc.Code` に定義されている Protobuf の Enum になります。 エラーコードの使い分けなどについては[protobufスキーマとgRPC通信](/fields/the-system/apis.md)を参照してください。

#### 複雑なエラー表現

さて、`GRPC::BadStatus` と `Google::Rpc::Code` を利用することでメッセージとエラーコードからなる単純なエラーを表現することは出来ました。 しかし、これでは実際のアプリケーションで表現力が足りません。 例えば単純なフォームのバリデーションエラーでも、フィールドごとにエラーメッセージを保持できる必要があります。

gRPC におけるエラーは実際には `google.rpc.Status` に定義されるような Protobuf message で表されます。

```protobuf3
message Status {
  // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
  int32 code = 1;

  // A developer-facing error message, which should be in English. Any
  // user-facing error message should be localized and sent in the
  // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
  string message = 2;

  // A list of messages that carry the error details.  There is a common set of
  // message types for APIs to use.
  repeated google.protobuf.Any details = 3;
}
```

この `google.rpc.Status` のうち、 Any の配列である `details` にエラーの詳細を詰めることが可能です。 [Google API Design Guide の Errors](https://cloud.google.com/apis/design/errors) では [google/rpc/error\_details.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto) に定義されるメッセージの利用が推奨されており、ウォンテッドリーでもそれに則っています。

Ruby でのエラー詳細の扱いに関しては、the\_pb gem で `.new_rpc_error` という関数が提供されており、それを使うと便利です。

```ruby
raise Pb.new_rpc_error(
  "post is invalid",
  code: Google::Rpc::Code::INVALID_ARGUMENT,
  details: [
    Google::Rpc::BadRequest.new(
      field_violations: [{ field: 'title', description: 'タイトルは必須です' }],
    )
  ],
)
```

### テスト

gRPC の RPC の実装は見ての通り普通のメソッドなので、最も単純には普通にメソッド呼び出しをして、返り値に対してテストをすることが出来ます。

```ruby
require 'rails_helper'

RSpec.describe ProfileGrpcService do
  let(:grpc_service) { described_class.new }
  let(:call) { instance_double("GRPC::ActiveCall::SingleReqView", metadata: {}) }

  describe '#batch_get_profiles' do
    let(:request) { W::UsersPb::BatchGetProfilesRequest.new(user_ids: user_ids) }
    let(:response) { grpc_service.batch_get_profiles(request, call) }
    let(:user_ids) do
      # ...
    end

    it 'returns user profiles' do
      # ...
    end
  end
end
```

ただ、これだと Protobuf の marshal / unmarshal 含め gRPC の内部実装を全く介さないため、 もう少し丁寧にやるなら `GrpcTesting` module[^1] (private library) の利用を検討するといいでしょう。

```ruby
require 'rails_helper'

RSpec.describe ProfileGrpcService do
  include GrpcTesting
  let(:stub) { test_stub_class(described_class.superclass.module_parent).new(described_class.new) }

  describe '#batch_get_profiles' do
    let(:response) { stub.batch_get_profiles(request, call) }

    # ...
  end
end
```

### 起動・デバッグ

次に、gRPC Server Process を起動します。gRPC Server を起動して Request を処理させるには、`app/grpc_services` に定義した「gRPC Service Class を継承した Class」を Handler として登録する必要があります。また、ウォンテッドリーでは、マイクロサービスは Kubernetes 上で Docker Container として動かしているので、Heath Check の為に gRPC Health Checking Protocol を実装した Handler の登録も必要です。その他、Observability を高めるための gRPC interceptor（gRPC の拡張機能。後述するが、Rack Middleware 相当のもの）の設定なども行う必要があります。

こういった「どの repository でも共通で必要になる処理」については、個々の開発者が気にする必要がないよう、[servicex (internal)](https://github.com/wantedly/servicex-ruby) と呼ばれる社内 library (gem) の機能として提供するようにしています。具体的には、grpc-server という名前のコマンドを用意しており、それを実行するだけで「gRPC Service Class の load および Handler としての登録、その他に共通で必要になる各種設定」が行われるようにしています。

`grpc-server` コマンドは以下のように利用することができます。

```
$ bundle exec grpc-server
handling /grpc.health.v1.Health/Check with #<Method: Grpc::Health::Checker#check>
handling /grpc.health.v1.Health/Watch with #<Method: Grpc::Health::Checker(Grpc::Health::V1::Health::Service)#watch>
handling /wantedly.users.UserService/ListUsers with #<Method: UsersGrpcService#list_users>
.
.
.
gRPC server starting...
* Listening on tcp://0.0.0.0:6046
* Environment: development
Use Ctrl-C to stop
```

この `grpc-server` コマンドのうち、Health Checker やシグナルハンドリングなどの設定を隠蔽したものは [grpc\_server](https://github.com/wantedly/grpc_server) gem として公開されています。

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

JSON/HTTP な API であれば cURL コマンドや [Insomnia](https://insomnia.rest/), [Postman](https://www.postman.com/) といったツールで実際にリクエストを送ってデバッグすることがあるかもしれません。 gRPC server では[標準でコマンドラインツールが提供されている](https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md) ほか、[grpcurl](https://github.com/fullstorydev/grpcurl) や [evans](https://github.com/ktr0731/evans) といった 3rd party のツールが存在します。 一方でこれらのツールでは「元になった .proto ファイル」もしくは「Server Reflection に対応した gRPC server」が必要になり、ウォンテッドリーの環境では少し使いづらいという問題がありました。

そこでウォンテッドリーでは [giro](https://github.com/rerost/giro) というツールを利用し、なるべく少ない手順・依存で gRPC server にリクエストを送れるような環境になっています。詳しくは [giro の解説をしているブログ](https://www.wantedly.com/companies/wantedly/post_articles/310167) もしくは [giro の使い方ドキュメント (internal)](https://github.com/wantedly/apis-reflection-server) を参照してください。

## 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 を単純に利用するだけであれば、以下のコードで動きます。

```ruby
client = W::UsersPb::ProfileService::Stub.new(url, :this_channel_is_insecure)
```

ただし、Production の Microservices で利用する上では「必ずセットして欲しい gRPC interceptor（gRPC の拡張機能。後述するが、Faraday Middleware 相当のもの）」などが存在するので、個々の開発者が意識しなくてもそれらの設定が自動で行われる様に、servicex gem の中で `.stub_for` という「gRPC Client 生成用のメソッド」を用意しています。

```ruby
client = Servicex::Grpc.stub_for(W::UsersPb::ProfileService, grpc_url)
```

`.stub_for` の実装はだいたい以下のようになっています。 元々、HTTP/1.1 の通信では servicex gem の中で Faraday Middleware などの設定を行なった API Client を提供する様にしていました。 `.stub_for` は同様の体験を gRPC でも提供することが意図されています。 Observability を確保するための interceptor のセットや User Agent の設定なども `.stub_for` で自動で行われるようになっています。

```ruby
# @example
#    Servicex::Grpc.stub_for(W::UsersPb::ProfileService, UsersApi.base_url)
# @param service_class [Class<Grpc::GeneralService>]
# @param url [String]
# @yield [opts] To modify options for instantiating a Stub
# @yieldparam opts [Hash] Default options
def stub_for(service_class, url)
  opts = {
    channel_args: {
      'grpc.primary_user_agent' => Servicex.user_agent,
    },
    interceptors: Servicex::Grpc.client_interceptors,
  }
  yield opts if block_given?
  service_class::Stub.new(url, :this_channel_is_insecure, opts)
end
```

gRPC Client については、基本的にやる事はこれだけです。 「apis と servicex を gem として load すれば、どのマイクロサービスからでも簡単に必要な設定が行われた状態で gRPC での通信が出来る」という環境を作っています。

最終的にはだいたい以下のようなコードを書くことになります（次節で説明するエラーハンドリングをしない場合のパターンです）。

```ruby
client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,  # servicex が提供する、ほかのマイクロサービスの URL を取得する関数
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

data = client.batch_get_profiles(req)
data  # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
```

### gRPC Client のエラーハンドリング

gRPC における通信の失敗は gRPC のエラーコードとして表されます。 ([Handbook - gRPC 通信のエラーハンドリング](https://docs.wantedly.dev/fields/system/apis#grpc-noerhandoringu) にその種類と、デフォルトで考慮すべきものがどれかの指針が書かれているので、確認しておいてください。)

マイクロサービス・アーキテクチャでの開発を行う際には、障害の分離のために、通信の失敗について考慮する必要があります。 通信が失敗した際、全体を失敗させるのではなくなんらか部分的に処理が継続可能な場合は、メソッド呼び出しで errors に想定しているエラーの種類を列挙してください。

これは、

```ruby
client = Servicex::Grpc.stub_for(
  W::Users::ProfileService,
  UsersGrpcApi.base_url,
  current_user_id: current_user.id,
)
req = W::Users::BatchGetProfileRequest.new(user_ids: user_ids)

res = client.batch_get_profiles(req, errors: [:deadline_exceeded])
```

これは、GRPC のエラーの種類に対応するシンボルとして指定できます。例えば、GRPC のエラーである DEADLINE\_EXCEEDED に対応するシンボルは `:deadline_exceeded` です。 また、サーバー側に原因があると考えられる UNKNOWN, INTERNAL, UNAVAILABLE を `:server_errors` でまとめて指定できるようにしています。

多くのケースでは、`errors: [:server_error, :deadline_exceeded]` を書くことをまず考えるのが良いプラクティスでしょう。コードレビューの際にも、そのことをチェックしてください。

errors を指定すると成功と失敗を表現するオブジェクトが返ってくるので、パターンマッチで処理しましょう。

```ruby
res = client.batch_get_profiles(req, errors: [:server_errors, :deadline_exceeded])
case resp
in Result::Success(data)
  data # => <UsersPb::BatchGetProfilesResponse: profiles: [...]>
in Result::Failure(error)
  # Failure の場合は自動で Honeybadger に送られます。
  error # => [<W::Users::ProfileServiceError: ...>]
end
```

エラーの種類によって個別の処理をしたい場合は、こういう風にパターンマッチを書くと良いでしょう。

```ruby
res = client.update_profile(req, errors: [:server_errors, :deadline_exceeded, :permission_denied])
case resp
in Result::Success(data)
  data
in Result::Failure(type: :permission_denied, error: _error)
  Pb::Visit::ToastError.new(message: "このデータを更新する権限がありません")
in Result::Failure(_error)
  Pb::Visit::ToastError.new(message: "更新が行なえませんでした。しばらくしてからもう一度やり直してください。")
end
```

### gRPC Client のモック

多くの場合、テストコード中ではマイクロサービス間通信を含む外部へのリクエストを禁止したくなります。 HTTP であれば [webmock](https://github.com/bblimke/webmock) gem を使うことが多いですが、gRPC では [grpc\_mock](https://github.com/ganmacs/grpc_mock) という gem を利用しています。

```ruby
before do
  GrpcMock.stub_request('/wantedly.users.ProfileService/BatchGetProfiles').to_return do |req, call|
    W::UsersPb::BatchGetProfilesResponse.new(
      profiles: [],
    )
  end
end
```

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

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

* [the\_pb](https://github.com/wantedly/the_pb)
  * Ruby で Protocol Buffers を扱う際に便利なユーティリティを集めた gem
* [pb-serializer](https://github.com/wantedly/pb-serializer)
  * ActiveModelSerializer ライクな DSL で Ruby オブジェクトを Protobuf に変換することができる
* [computed\_model](https://github.com/wantedly/computed_model)
  * pb-serializer と組み合わせることで、データソースを抽象化したデータの読み込みや GraphQL ライクな field selector が実装できる
* Observability を支える interceptor たち
  * [grpc\_newrelic\_interceptor](https://github.com/wantedly/grpc_newrelic_interceptor)
  * [grpc\_opencensus\_interceptor](https://github.com/wantedly/grpc_opencensus_interceptor)
  * [grpc\_access\_logging\_interceptor](https://github.com/wantedly/grpc_access_logging_interceptor)
* [reloader\_interceptor](https://github.com/wantedly/reloader_interceptor)
  * gRPC server 開発で Rails のような hot-reloading を実現する
* [grpc\_typechecker: A dynamic type checker for gRPC methods](https://github.com/wantedly/grpc_typechecker)
  * RPC が返しているオブジェクトが正しい Protobuf messaage かどうかを検証する interceptor
  * see also [gRPC Ruby でハマらないための型チェッカー | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/298998)
* [grpc\_server](https://github.com/wantedly/grpc_server)
  * gRPC server を起動するためのボイラプレートを隠蔽した gem
* [rerost/giro](https://github.com/rerost/giro)
  * 簡単に gRPC server を叩ける CLI
  * [Server Reflectionが実装されていないgRPCサーバーでも簡単に叩けるCLIを作る | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/310167)

#### 話を聞きに行きたい

* Slack: [#rubyist](https://wantedly.slack.com/archives/CB734C068)

#### もっと知りたい

* Wantdly の Rails のアーキテクチャについて理解したい
  * [マイクロサービス・アーキテクチャと共存する Ruby on Rails のアーキテクチャ的拡張 - その事例と可能性 / The Architectural Extension of Ruby on Rails to fit to microservices - Speaker Deck](https://speakerdeck.com/altech/the-architectural-extension-of-ruby-on-rails-to-fit-to-microservices)
* gRPC について知りたい
  * [Real World Performance of gRPC - gRPC 利用による劇的なパフォーマンス改善 | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/220495)
  * [gRPC Internal - gRPC の設計と内部実装から見えてくる世界 | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/219429)

[^1]: <https://github.com/wantedly/users-rails/blob/master/spec/support/grpc\\_testing.rb>
