【Go】gRPC から REST API Server をつくる

今回は Go で gRPC から REST API Server をつくる方法の備忘録です。

Go のバージョンは 1.11 です。

$ go version
go version go1.11 darwin/amd64

gRPC

gRPC (gRPC Remote Procedure Calls) は Google が開発した OSS の RPC (remote procedure call) フレームワーク。

grpc.io では主な特徴として以下が紹介されている。

  • Idiomatic client libraries in 10 languages
  • Highly efficient on wire and with a simple service definition framework
  • Bi-directional streaming with http/2 based transport
  • Pluggable auth, tracing, load balancing and health checking

http/2 による双方向ストリーミングが可能で認証や LB などプラガブルに機能拡張できる機構を持っている。また, サービス定義はシンプルで10言語のクライアントライブラリをサポートしているため特定の言語やプラットフォームに依存しない。

データシリアライゼーションには同じく Google の Protocol Buffers が使われている。

Protocol Buffers

Protocol Buffers はシリアライズフォーマットで, その構造を定義するためのインタフェース記述言語 (Interface Description Language) でもある。 .proto ファイルに Message や Services を定義する。言語仕様は Language Guide (proto3) を確認する。

releases から 環境に合わせた zip を選択しインストールする。 grpc-gateway を使うために ProtocolBuffers 3.0.0-beta-3 以降のバージョンが必要。

$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-osx-x86_64.zip
$ mkdir protoc-3.6.1
$ unzip protoc-3.6.1-osx-x86_64.zip -d protoc-3.6.1/
Archive:  protoc-3.6.1-osx-x86_64.zip
   creating: protoc-3.6.1/include/
   creating: protoc-3.6.1/include/google/
   creating: protoc-3.6.1/include/google/protobuf/
  inflating: protoc-3.6.1/include/google/protobuf/wrappers.proto
  inflating: protoc-3.6.1/include/google/protobuf/field_mask.proto
  inflating: protoc-3.6.1/include/google/protobuf/api.proto
  inflating: protoc-3.6.1/include/google/protobuf/struct.proto
  inflating: protoc-3.6.1/include/google/protobuf/descriptor.proto
  inflating: protoc-3.6.1/include/google/protobuf/timestamp.proto
   creating: protoc-3.6.1/include/google/protobuf/compiler/
  inflating: protoc-3.6.1/include/google/protobuf/compiler/plugin.proto
  inflating: protoc-3.6.1/include/google/protobuf/empty.proto
  inflating: protoc-3.6.1/include/google/protobuf/any.proto
  inflating: protoc-3.6.1/include/google/protobuf/source_context.proto
  inflating: protoc-3.6.1/include/google/protobuf/type.proto
  inflating: protoc-3.6.1/include/google/protobuf/duration.proto
   creating: protoc-3.6.1/bin/
  inflating: protoc-3.6.1/bin/protoc
  inflating: protoc-3.6.1/readme.txt
$ ln -s ~/tmp/protoc-3.6.1/bin/protoc /usr/local/bin/protoc
$ which protoc
/usr/local/bin/protoc
$ protoc --version
libprotoc 3.6.1

protoc コマンドは protocol buffer compiler で .proto ファイルを parse し各言語のコードを生成する機能を持っている。

gRPC-Go

gRPC の Go 実装である gRPC-Go をインストールする。

go get -u google.golang.org/grpc

google.golang.org/grpc は後述する EchoServiceServer の実装で使う。

grpc-gateway

grpc-gateway は gRPC サービス定義 (.proto) を基に gRPC を RESTful JSON API に翻訳する reverse-proxy server を生成するツール。

(出展: github.com/grpc-ecosystem/grpc-gateway)

まず, grpc-gateway のインストール。

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
$ go get -u github.com/golang/protobuf/protoc-gen-go

今回は examples にある Echo Service を例とする。 (echo_service.proto)

syntax = "proto3";
option go_package = "examplepb";

// Echo Service
//
// Echo Service API consists of a single service which returns
// a message.
package grpc.gateway.examples.examplepb;

import "google/api/annotations.proto";

// Embedded represents a message embedded in SimpleMessage.
message Embedded {
	oneof mark {
		int64 progress = 1;
		string note = 2;
	}
}

// SimpleMessage represents a simple message sent to the Echo service.
message SimpleMessage {
	// Id represents the message identifier.
	string id = 1;
	int64 num = 2;
	oneof code {
		int64 line_num = 3;
		string lang = 4;
	}
	Embedded status = 5;
	oneof ext {
		int64 en = 6;
		Embedded no = 7;
	}
}

// Echo service responds to incoming echo requests.
service EchoService {
	// Echo method receives a simple message and returns it.
	//
	// The message posted as the id parameter will also be
	// returned.
	rpc Echo(SimpleMessage) returns (SimpleMessage) {
		option (google.api.http) = {
			post: "/v1/example/echo/{id}"
			additional_bindings {
				get: "/v1/example/echo/{id}/{num}"
			}
			additional_bindings {
				get: "/v1/example/echo/{id}/{num}/{lang}"
			}
			additional_bindings {
				get: "/v1/example/echo1/{id}/{line_num}/{status.note}"
			}
			additional_bindings {
				get: "/v1/example/echo2/{no.note}"
			}
		};
	}
	// EchoBody method receives a simple message and returns it.
	rpc EchoBody(SimpleMessage) returns (SimpleMessage) {
		option (google.api.http) = {
			post: "/v1/example/echo_body"
			body: "*"
		};
	}
	// EchoDelete method receives a simple message and returns it.
	rpc EchoDelete(SimpleMessage) returns (SimpleMessage) {
		option (google.api.http) = {
			delete: "/v1/example/echo_delete"
		};
	}
}

protoc コマンドを使い .proto から gRPC stub (echo_service.pb.go) を生成する。

$ protoc -I/usr/local/bin -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  proto/echo_service.proto

EchoServiceServer の実装。 (echo.go)

package main

import (
	"context"

	"github.com/golang/glog"
	examples "../proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

// Implements of EchoServiceServer

type echoServer struct{}

func newEchoServer() examples.EchoServiceServer {
	return new(echoServer)
}

func (s *echoServer) Echo(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
	glog.Info(msg)
	return msg, nil
}

func (s *echoServer) EchoBody(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
	glog.Info(msg)
	grpc.SendHeader(ctx, metadata.New(map[string]string{
		"foo": "foo1",
		"bar": "bar1",
	}))
	grpc.SetTrailer(ctx, metadata.New(map[string]string{
		"foo": "foo2",
		"bar": "bar2",
	}))
	return msg, nil
}

func (s *echoServer) EchoDelete(ctx context.Context, msg *examples.SimpleMessage) (*examples.SimpleMessage, error) {
	glog.Info(msg)
	return msg, nil
}

gRPC Service を起動する Server の実装。 (server.go)

package main

import (
	"context"
	"flag"
	"net"

	examples "../proto"
	"github.com/golang/glog"
	"google.golang.org/grpc"
)

// Run starts the example gRPC service.
// "network" and "address" are passed to net.Listen.
func run(ctx context.Context, network, address string) error {
	l, err := net.Listen(network, address)
	if err != nil {
		return err
	}
	defer func() {
		if err := l.Close(); err != nil {
			glog.Errorf("Failed to close %s %s: %v", network, address, err)
		}
	}()

	s := grpc.NewServer()
	examples.RegisterEchoServiceServer(s, newEchoServer())

	go func() {
		defer s.GracefulStop()
		<-ctx.Done()
	}()
	return s.Serve(l)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	ctx := context.Background()

	if err := run(ctx, "tcp", ":9090"); err != nil {
		glog.Fatal(err)
	}
}

gRPC Server を起動する。

$ go run server.go echo.go

gRPC Client

gRPC Server の疎通確認のため gRPC Client から接続してみる。 grpc_cli はインストールのためにビルドが必要で時間がかかるため Evans を選択した。

$ go get -u github.com/ktr0731/evans

.proto の配置を以下のようにした。 (google/api/annotations.proto, google/api/http.proto)

$ tree -P *.proto proto/ google/
proto/
└── echo_service.proto
google/
└── api
    ├── annotations.proto
    └── http.proto

1 directory, 3 files

gRPC Server を起動している状態で Evans を REPL Mode で起動する。

$ evans --port 9090 proto/echo_service.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


127.0.0.1:9090> show package
+---------------------------------+
|             PACKAGE             |
+---------------------------------+
| grpc.gateway.examples.examplepb |
| google.api                      |
+---------------------------------+

127.0.0.1:9090> package grpc.gateway.examples.examplepb

grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> show service
+-------------+------------+---------------+---------------+
|   SERVICE   |    RPC     |  REQUESTTYPE  | RESPONSETYPE  |
+-------------+------------+---------------+---------------+
| EchoService | Echo       | SimpleMessage | SimpleMessage |
|             | EchoBody   | SimpleMessage | SimpleMessage |
|             | EchoDelete | SimpleMessage | SimpleMessage |
+-------------+------------+---------------+---------------+

grpc.gateway.examples.examplepb@127.0.0.1:9090> service EchoService

grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> show message
+---------------+
|    MESSAGE    |
+---------------+
| Embedded      |
| SimpleMessage |
+---------------+

grpc.gateway.examples.examplepb.EchoService@127.0.0.1:9090> call Echo
? code line_num
line_num (TYPE_INT64) => 1
? ext en
en (TYPE_INT64) => 1
id (TYPE_STRING) => 1
num (TYPE_INT64) => 1
? mark note
status::note (TYPE_STRING) => 1
{
  "id": "1",
  "num": 1,
  "lineNum": 1,
  "lang": "",
  "status": {
    "progress": 0,
    "note": "1"
  },
  "en": 1,
  "no": null
}

gRPC Server のデバッグに便利。

RESTful JSON API Server

次に protoc コマンドを使い reverse-proxy (echo_service.pb.gw.go) を生成する。

$ protoc -I/usr/local/bin -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  proto/echo_service.proto

echo_service.pb.gw.go が gRPC を RESTful JSON API に翻訳する機能を提供する。
gRPC Service への接続と HTTP Server を起動する部分の実装が以下。 (api_server.go)

package main

import (
  "flag"
  "net/http"

  "github.com/golang/glog"
  "golang.org/x/net/context"
  "github.com/grpc-ecosystem/grpc-gateway/runtime"
  "google.golang.org/grpc"

  gw "../proto"
)

var (
  echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
)

func run() error {
  ctx := context.Background()
  ctx, cancel := context.WithCancel(ctx)
  defer cancel()

  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithInsecure()}
  err := gw.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
  if err != nil {
    return err
  }

  return http.ListenAndServe(":8080", mux)
}

func main() {
  flag.Parse()
  defer glog.Flush()

  if err := run(); err != nil {
    glog.Fatal(err)
  }
}

gRPC Server が起動している状態で, API Server を起動する。

$ go run api_server.go

API Server の確認のため HTTP Request を投げてみる。

$ curl -vvv -X POST "http://127.0.0.1:8080/v1/example/echo/hoge" -H "accept: application/json"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /v1/example/echo/hoge HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> accept: application/json
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Grpc-Metadata-Content-Type: application/grpc
< Date: Sun, 23 Sep 2018 01:41:09 GMT
< Content-Length: 13
<
* Connection #0 to host 127.0.0.1 left intact
{"id":"hoge"}

request path で使用していない message のフィールドは query parameter (e.g., `?en=1&status.note=abc`) として与えることでメソッド内で拾える。

$ curl -vvv -X POST "http://127.0.0.1:8080/v1/example/echo/hoge?en=1&status.note=done" -H "accept: application/json"
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /v1/example/echo/hoge?en=1&status.note=done HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> accept: application/json
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Grpc-Metadata-Content-Type: application/grpc
< Date: Sun, 23 Sep 2018 01:43:10 GMT
< Content-Length: 47
<
* Connection #0 to host 127.0.0.1 left intact
{"id":"hoge","status":{"note":"done"},"en":"1"}

gRPC から REST API Server をつくる部分は以上で完了。

Swagger

Swagger は REST API を記述するための仕様。 grpc-gateway の機能を使って Swagger による REST API 仕様を生成することができる。

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

protoc コマンドで echo_service.swagger.json を生成する。

$ protoc -I/usr/local/bin -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --swagger_out=logtostderr=true:. \
  proto/echo_service.proto

Swagger Editor を DL し起動する。

$ wget https://github.com/swagger-api/swagger-editor/archive/v3.6.10.zip
$ unzip v3.6.10.zip
$ npm install http-server
$ mv swagger-editor-3.6.10 swagger-editor
$ ./node_modules/.bin/http-server swagger-editor

起動後, echo_service.swagger.json を読み込む。 Swagger Editor は編集しながらプレビューを確認でき便利。

[1] Swagger UI
[2] grpcc
[3] Proto3 Language Guide(和訳)
[4] grpc-gatewayの開発に学ぶ、ソフトウェアの設計手法