Ruby on Rails ✖️ Docker ✖️ gRPC

# はじめに
メタップスアドベントカレンダー20日目の記事です。

弊社では一部マイクロサービスになっており、その中でRuby on Railsのアプリケーションが動いています。
RailsアプリはクライアントとしてgRPCとProtoBufを使ってgoのサーバーと通信しています。
今回はRails on DockerのサービスにgRPCを導入する大まかな手順と運用をしていく中での知見を2点、紹介します。

#### 注意
- 「gRPCとは何か」や「ProtoBufの書き方」は紹介しません
- RailsアプリがDocker(docker comopse)上で動いているものとします
- コードは全てサンプルなので参考程度にとどめてください

# Rails on Docker へのgRPCの導入
#### protoファイルの作成

```sample.proto
syntax = "proto3";
package sample;

service SampleService {
    rpc RequestSomething(SampleRequest) returns (SampleResponse) {}
}

message SampleRequest {
    string sampleStringRequest = 1;
}

message SampleResponse {
    string sampleStringResponse = 1;
}
```
上記は文字列を送ったら文字列が返ってくるServiceを作成するサンプルです。
ProtoBufのおかげでインターフェイスがわかりやすい！

#### stubファイルの作成

https://rubygems.org/gems/grpc-tools

こちらのgemを使えばprotoを元にstubファイルを作成できます。
protoファイルを/proto配下に置いて、生成されるstubファイルを/sevices配下に置く場合は以下のような感じ。

```Dockerfile.protoc
FROM ruby:3.1.4

WORKDIR /usr/local/src

RUN gem install grpc grpc-tools

COPY sample/setup.sh /usr/local/bin
RUN chmod +x /usr/local/bin/setup.sh

CMD ["/usr/local/bin/setup.sh"]
```

```setup.sh
#!/usr/bin/env bash
set -eu
grpc_tools_ruby_protoc --ruby_out=./services --grpc_out=./services proto/sample.proto 
```

```compose.yml
services:
  rails-app:
  # 中略
  protoc-sample:
    platform: linux/x86_64 # Appleシリコンの場合
    build:
      dockerfile: ./sample/Dockerfile.protoc
    volumes:
      - .:/usr/local/src
```
コンテナ内で必要なgemをインストールしてsetup.shでコマンドを実行しているだけですね。
```sh
$ docker compose run --rm protoc-sample
```

そうするとこんな感じになります。
```
sample
├── Dockerfile.protoc
└── setup.sh
services
└── proto
    ├── sample_pb.rb
    └── sample_services_pb.rb
proto
└── sample.proto
compose.yml
```
自動でstubファイルが生成されました！

#### initializers配下の修正

```config/initializers/grpc.rb
require './services/proto/sample_services_pb.rb'
```

こうすることでRails内でstubファイルで定義されたクラスが使えます。

```どこか.rb
stub = Sample::SampleService::Stub.new({{gRPCサーバーのエンドポイント}}, :this_channel_is_insecure)
stub.request_something("hogehoge")
```
これでクライアントとしてgRPCサーバーを呼び出せるようになりました!

# モックについて
マイクロサービスで大変なのはテストですよね。
一機能をテストするたびにローカルでさまざまなアプリケーションを立ち上げてたら大変。
それもDockerで動いていたらメモリが足りません。

そんなときgRPCのツールが提供してくれる機能は非常に役に立ちます。

先ほど作成したstubファイルからgRPCサーバーを立てることができます。
公式tutorialを見ながらやればそんなに難しくない！

https://grpc.io/docs/languages/ruby/basics/#implementing-routeguide

```mock/sample_server.rb
require './proto/sample_services_pb.rb'

class ServerImpl < Sample::SampleService::Service
  def sample_request(req, call)
    Sample::SampleResponse.new(sampleStringResponse: "fugafuga")
  end
end

server = GRPC::RpcServer.new
server.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)
server.handle(ServerImpl.new)
server.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
```
こんな感じで書けばlocalhost:50051で立ち上がります。
今回はレスポンスを"fugafuga"にしています。

Dockerを使っている場合はこいつをRailsアプリと一緒に立ち上げたいですよね。
その場合はcompose.ymlに追記。
```compose.yml
services:
  rails-app:
  # 中略
　　　　protoc-sample:
  # 中略
  sample_mock:
    command: bundle exec ruby ./mock/sample_server.rb
    ports:
      - 50051:50051
```

これでdocker compose upでgRPCのモックサーバーが立ち上がります！

```どこか.rb
stub = Sample::SampleService::Stub.new('sample_mock:50051', :this_channel_is_insecure)
stub.request_something("hogehoge")
```

これで他のマイクロサービスを立ち上げなくてもいい！
CIで自動テストする際も楽になるかと思います。


# protoファイルの管理
これも悩ましい問題ですよね。

上記ではクライアント側でもprotoファイルを持っています。
このままではインターフェイスが変わるたびにサーバー側もクライアント側もどちらも変更しなくてはいけません。（絶対漏れる）

調べた中でも色々と方法があるようです。

https://speakerdeck.com/line_developers/starting-grpc?slide=49

https://qiita.com/nakaryooo/items/d71d2cb5f5cd44b052ee

protoファイルのみを管理するリポジトリを作成する方法もあるみたいですね。

ただ弊社ではそこまで人数が多くないことと日々のコミュニケーションが活発なのでここまではしていません。

基本的にはサーバー側でprotoファイルを管理しています。

そしてクライアント側ではprotoファイルを持たずにdocker compose upのタイミングでstubファイルを更新するようにしています。
具体的にはどうしているかというと以下のような感じです。


```Dockerfile.protoc
FROM ruby:3.1.4

WORKDIR /usr/local/src

ARG GITHUB_OAUTH_TOKEN
ARG BRANCH
ARG REPOSITORY
ENV REPOSITORY ${REPOSITORY}

RUN gem install grpc grpc-tools

RUN git clone https://$GITHUB_OAUTH_TOKEN:x-oauth-basic@/${REPOSITORY} \
 && cd ${REPOSITORY} \
 && git checkout ${BRANCH}

COPY etc/docker/sample/setup.sh /usr/local/bin
RUN chmod +x /usr/local/bin/setup.sh

WORKDIR {{　clone先のルートディレクトリ　}}

CMD ["/usr/local/bin/setup.sh"]
```


```setup.sh
#!/usr/bin/env bash
set -eu

# 更新
cd /usr/local/src/${REPOSITORY}

echo '$ git pull'
git pull

echo "grpc_tools_ruby_protoc --ruby_out=./proto --grpc_out=./proto /usr/local/src/${REPOSITORY}/proto/${REPOSITORY}.proto"
grpc_tools_ruby_protoc --ruby_out=./proto --grpc_out=./proto /usr/local/src/${REPOSITORY}/proto/${REPOSITORY}.proto
```

```compose.yml
services:
  rails-app:
  # 中略
  sample_mock:
  # 中略
  protoc-sample:
    platform: linux/x86_64
    build:
      dockerfile: ./sample/Dockerfile.protoc
      args:
        - GITHUB_OAUTH_TOKEN=${GITHUB_OAUTH_TOKEN}
        - REPOSITORY=${REPOSITORY}
        - BRANCH=${BRANCH}
    volumes:
      - .:/usr/local/src
```

新たな環境変数に`REPOSITORY`,`GITHUB_OAUTH_TOKEN`, `BRANCH`が追加されました。
大まかな流れとしては、サーバー側のリポジトリをclone→protoからstubファイルを作成→ホストにバインドという流れです。


使い方はこんな感じ

1 https:\//github.com/settings/tokens でtokenを発行する
2 環境変数に設定する
```
GITHUB_OAUTH_TOKEN={{ 取得したtoken }}
REPOSITORY={{ リポジトリのパス }}
BRANCH={{ ブランチ }}
```

3 docker compose upするだけ

```zsh
docker compose up
```

いい感じですね。
ただupしっぱなしの時は更新されないので注意は必要です。
またマイクロサービス間でディレクトリ構成がある程度共通していることが前提です。

もっとマイクロサービスや開発者が増えたら管理方法を変更していく必要はあるかもしれません。

以上Rails ✖️ Docker ✖️ gRPCの導入から運用のTipsでした！