SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

コピペでできるGoでgRPCサーバ立ててRailsからアクセスする方法

最近社内でElixirをひっそり布教しようとしている、瀧川です。

弊社の一部プロダクトでは、gRPCでGolangアプリケーションを呼び出す構成をとっています。

それを説明するためにハンズオンをしたので、その一連の流れをこちらにもつらつら書いていきます!

内容は、以下の画像みたいなのを作っていこうと思います。

必要なコードは記事中に載せるのでコピペで動くはずなので、ぜひ実際にやってみてください!

f:id:tkgwy:20190328170303p:plain

そもそもgRPCって?

Googleが開発している、RPC (Remote Procedure Call)を実現するためのプロトコルになります。 Protocol Buffersというシリアライズのフォーマットを用いて、通信(関数・引数・返り値)を定義する仕様となっています。

対応するプログラミング言語も豊富で、その定義をもとにクライアント(呼び出し側)・サーバ(呼び出される側)のソースコードの生成が可能で、安全で効率よくサーバ間通信を行うことができます。

今回の例に当てはめて簡単に表現すると「RailsからGolangのメソッドを普通に呼び出せる」といったニュアンスですね!(雑)

詳しくは以下リンクを参考にしてください。

公式サイト: Guides | gRPC

実践

事前にインストールしてください

以下をインストールしましょう。(versionは2019-03-27時点の筆者のもの)

  • Ruby: 2.5.3
  • Golang: 1.11.4
  • protobuf: 3.7.0

リポジトリを作成しましょう

アプリケーションとしては、RailsのJSON APIサーバと、GolangのgRPCサーバの2つになりますが、今回は単一リポジトリで実装していきましょう!

ざっくりディレクトリ構成

grpc-sample
├ proto
│ └ ProtocolBuffers定義
├ server
│ ├ Railsアプリ
│ └ lib
│   └ protoから自動生成
└ pinger
  ├ Golangアプリ
  └ lib
    └ protoから自動生成
$ mkdir grpc-sample
$ cd grpc-sample

さっそくProtocol Buffersで通信を定義しましょう

肝であるProtocol Buffersで通信(関数・引数・返り値)を定義していきましょう。

「引数なしでテキストを返すPing関数」の定義が以下のようになります。

簡単ですね!

grpc-sample/proto/pinger.proto

syntax = "proto3";

package pinger;

service Pinger {
  rpc Ping(Empty) returns (Pong) {}
}

message Empty {}

message Pong {
  string text = 1;
}

GolangのgRPCサーバ作りましょう

ディレクトリを作成

# cd grpc-sample
$ mkdir pinger
$ mkdir pinger/lib
$ cd pinger

必要ライブラリをインストール

# gRPCサーバの実装に必要
$ go get -u google.golang.org/grpc
# Protocol Buffersの定義からGolangのソースコードを生成に必要
$ go get -u github.com/golang/protobuf/protoc-gen-go

定義からGolangのソースコードを生成

いよいよ grpc-sample/proto/pinger.proto を元にGolangのインタフェースのソースコードを生成しましょう!

以下のコマンドによって grpc-sample/pinger/lib/pinger.pb.go が生成されます。

# cd grpc-sample
$ protoc -I ./proto pinger.proto --go_out=plugins=grpc:./pinger/lib
$ ls pinger/lib
pinger.pb.go

生成したGolangのインタフェースを実装したサーバを作成

コードの中身としては、以下を実装しています。

  • 生成されたインタフェース(Ping関数)を実装したStructを作成
  • 生成された pinger.RegisterPingerServer 関数でgRPCサーバと実装したStructを紐づけ
  • 5300番ポートでサーブ

grpc-sample/pinger/server.go

package main

import (
    "context"
    "log"
    "net"

    "./lib"
    "google.golang.org/grpc"
)

func main() {
    listener, err := net.Listen("tcp", ":5300")
    if err != nil {
        log.Fatalf("failed to listen: %v\n", err)
        return
    }
    grpcSrv := grpc.NewServer()
    pinger.RegisterPingerServer(grpcSrv, &server{})
    log.Printf("Pinger service is running!")
    grpcSrv.Serve(listener)
}

type server struct{}

func (s *server) Ping(ctx context.Context, req *pinger.Empty) (*pinger.Pong, error) {
    pong := &pinger.Pong{
        Text: "pong",
    }
    return pong, nil
}

実行してみましょう!

# cd grpc-sample/pinger
$ go run server.go
# 2019/03/27 19:53:21 Pinger service is running!

動いてるみたいですね!

動作確認

ちゃんと動作してるか、Golangで呼び出してチェックしてみましょう。

package main

import (
    "context"
    "fmt"
    "os"

    "../lib"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:5300", grpc.WithInsecure())
    if err != nil {
        fmt.Fprintf(os.Stderr, "grpc.Dial: %v\n", err)
        return
    }
    defer conn.Close()
    client := pinger.NewPingerClient(conn)
    req := &pinger.Empty{}
    pong, err := client.Ping(context.Background(), req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ping: %v\n", err)
        return
    }
    fmt.Fprintf(os.Stdout, "Pong: %s\n", pong.Text)
}

実行してみましょう!

別ターミナル等で go run server.go を実行するの忘れずに!

# cd grpc-sample/pinger
$ go run examples/client.go
# Pong: pong

大丈夫そうですね!

これでgRPCのサーバ実装は終了です。

若干のおまじないコードはあるかと思いますが、とても簡単に実装はできました。

Railsを実装しましょう

今回はRails5のAPIモードで作成しようと思います。

また、DB(Active Record)は 用意が面倒なので スキップします。

Railsをインストール

# cd grpc-sample
$ mkdir server
$ cd server
$ bundle init
$ echo 'gem "rails", ">=5.2.2.1"' >> Gemfile
$ bundle install
# -O でskip active record
$ bundle exec rails new . --api -O
$ bundle exec rails server
# localhost:3000 にアクセスするとおなじみの画面がでますね!

gRPC定義からRubyのソースコードを生成

必要なGemをインストールしましょう。

# cd grpc-sample/server
# 呼び出し時に必要
$ echo "gem 'grpc'" >> Gemfile
# 定義からのソースコード生成に必要
$ echo "gem 'grpc-tools'" >> Gemfile
$ bundle install
$ bundle exec grpc_tools_ruby_protoc -I ../proto --ruby_out=lib --grpc_out=lib ../proto/pinger.proto
$ ls lib
# pinger_pb.rb          pinger_services_pb.rb         tasks

生成されました!

呼び出すコードをControllerに実装

Stub.new でコネクションを貼って、あとはほぼオブジェクトへのメソッド呼び出しと同じように書けばOK。

grpc-sample/server/app/controllers/application_controller.rb

require 'pinger_services_pb.rb'
require 'pinger_pb.rb'

class ApplicationController < ActionController::API
  def ping
    pinger_stub = Pinger::Pinger::Stub.new('localhost:5300', :this_channel_is_insecure)
    pong = pinger_stub.ping(Pinger::Empty.new)
    render json: {pong: pong.text}
  end
end

grpc-sample/server/config/routes

Rails.application.routes.draw do
  get 'ping', to: 'application#ping'
end

通しで実行してみましょう

Golang gRPCサーバ起動

# cd grpc-sample/pinger
$ go run server.go
# 2019/03/27 19:53:21 Pinger service is running!

Rails JSON APIサーバ起動

# cd grpc-sample/server
$ bundle exec rails server
# Listening on tcp://localhost:3000

アクセスしてみると...

f:id:tkgwy:20190327205519p:plain

ほしい結果が返ってきました!

振り返り

再掲となりますが、以下のようなファイル構成で構築しました。

grpc-sample
├ proto
│ └ ProtocolBuffers定義
├ server
│ ├ Railsアプリ
│ └ lib
│   └ protoから自動生成
└ pinger
  ├ Golangアプリ
  └ lib
    └ protoから自動生成

やった内容は以下の通りです。

  1. proto ディレクトリにProtocol Buffersの仕様に則した通信(関数・引数・返り値)を定義
  2. Ruby用のソースコードと、Golang用のソースコードを 1. のprotoファイルを元に生成
  3. 2. で生成されたGolangのインタフェース定義に則してgRPCサーバを実装
  4. 2. で生成されたRubyのgRPCクライアント定義に則して呼び出すコードを実装

シンプルに動かすだけなら、そこまで複雑でなく、コード量も少ないというのが伝わりましたか?

APIのインタフェースを考えなくてよく、関数定義をすればよいというのは、開発をしていてもとても考える負荷が減って効率的だと感じます。

今後世の中で一層マイクロサービス化が進むと思いますが、その中でgRPCは重要な技術となると考えているので、みなさんトライしてみてください!