経緯
会社でPMを担当しているプロダクト(Rails)の実装で他のサービスから情報を取得する必要があり、そのサービスがOpenAPIでなく、gRPCでAPIインタフェースが定義されていたので、gRPCのクライアントを実装しました。
僕が前職でrailsでのgRPC周りを少し触っていたので、相対的に僕がやった方がチームの開発工数が増えると思ったので自分でやることにしました。
それについてのアウトプットです。
実装手順
1. gem導入
※前職で使っていたgrufというライブラリを使いました。
gem "google-protobuf"
gem "grpc-tools"
gem "gruf"
※ webというコンテナで開発してる
docker-compose run --rm web bundle install
2. protoファイルが管理されているリポジトリをsubmoduleでアプリケーションのサブディレクトリとして登録して、protoファイルを元のリポジトリから取ってくる
$ git submodule add [web URL or ssh key] proto
$ git submodule init
$ cd proto
$ git submodule update
3. protoファイルをrubyファイルにコンパイル
docker-compose run --rm web grpc_tools_ruby_protoc -I [コンパイル対象のprotoディレクトリ] --ruby_out=[コンパイル後のファイル保存先のディレクトリ] --grpc_out=[コンパイル後のファイル保存先のディレクトリ] [コンパイル対象のprotoディレクトリ内の対象ファイル]
4. コンパイル後のrubyファイル(*_pb.rb)の読み込み設定
コンパイル後のファイルは、ファイル名と、クラス名が噛み合っておらず、Railsの読み込み規則に則っていなく自動読み込まれないので、指定してあげる必要がある。
require "gruf"
Gruf.configure do
Dir.glob(Rails.root.join("[コンパイル後のファイル保存先のディレクトリ]/*_pb.rb")).each do |file|
require file
end
end
コンパイル後のファイルでは下記のように自動で指定されており、auto_load_path
に追加しておく必要がある。
e.g. gruf-demoから
require 'Products_pb'
※コンパイル後のファイルは基本修正しないので。
class Application < Rails::Application
config.paths.add [コンパイル後のrubyファイルディレクトリ], eager_load: true
end
5. クライアントがサーバをコールする部分の実装
ここまでで、全てのコンパイル後のrubyファイルは使えるようになったでのクライアントの実装。
moduleにしようかとか悩みましたが、既存の実装でクライアント系の処理はservice層にまとめていたので、今回もそれに習う形にしました。
※特にmetadata
周りはサーバ側の実装に依存するので注意。
grufのwikiだと、クライアントの初期化(Gruf::Client.new
)時のoptions
引数のキーでusername
を入れていたりとこの辺が今回の実装と違っており、若干悩みました。
class GrpcClientService
def initialize
@metadata = {
login: ENV["GRPC_CLIENT"],
password: ENV["GRPC_PASSWORD"]
}
end
def run(service_klass, method, request)
client = Gruf::Client.new(
service: service_klass,
options: {
hostname: ENV["GRPC_HOST"],
channel_credentials: :this_channel_is_insecure
}
)
client.call(method, request.to_h, @metadata)
end
end
導入してみての感想とか
前職でgrufは使っていたので、余裕かと思っていましたが、やはり導入するのと、ただ使うだけでは結構違うなと思いました。
しっかり設定周りのコードを読んでおけば良かったと後悔してます。
また、今回gRPCサーバがgoで書かれており、クライアントからのコールがうまく行かないときにコード読むのに苦労して結局諦めたので、goも勉強したいなと思いました。
また少し詰まったのは、クライアントの初期化(Gruf::Client.new
)時にmetadata
を入れると謎の勘違いしており(本当はcall時の引数に入れる)、ライブラリのWikiに書いてないことはやはり、しっかりコード読まないといけないなと初歩的なことを改めて実感しました。