はじめに
HTTP/JSONからgRPCへのtranscodingを行うには大まかに次の2種類の方法があります。
- gRPC gatewayを使用する。
- gRPC-JSON transcoderを使用する。
前者はプラグインとして提供されていてHTTPリバースプロキシを作成することになります。これはgolangにしか対応されていません。
後者についてはgrpc-httpjson-transcodingというライブラリがありIstioやGoogle cloud endpointで現在も使用されています。
envoy proxy単体でもgRPC serviceにHTTP/JSONインタフェースを適用することで実行可能です。
今回はenvoy, gRPCサーバをDockerコンテナとして立ててgRPC-JSON transcoderのテストを行います。HTTPリクエストはGET/POSTを試しています。
コードはGitHubにあげてあります。
環境
macOS Mojave 10.14.1
docker 18.09.2
docker-compose 1.23.2
実装
ビルド前に用意しておくファイルは次のようになります。envoyについては公式のDockerイメージを使用するので自分でDockerfileは作成しません。
├── docker-compose.yaml
├── pb
│ └── helloworld.proto
├── server
│ ├── Dockerfile
│ └── main.go
└── envoy
└── envoy.yaml
まずはdocker-compose.yaml
です。こちらはgRPCサーバとenvoyを定義しています。
envoyとgRPCサーバの間はコンテナ間通信を行うのでlinks
を設定します。
version: '3.7'
services:
server:
build: ./server
image: grpc-server:latest
container_name: grpc-server
volumes:
- './server:/go/src/server'
- './pb:/go/src/pb'
expose:
- 50051
command:
- /go/src/server/bin/server
envoy:
image: envoyproxy/envoy:v1.13.1
volumes:
- './envoy:/etc/envoy'
- './pb:/etc/pb'
expose:
- 51051
ports:
- 51051:51051
links:
- server
volumes:
data:
driver: 'local'
次にprotoファイルです。GETとPOSTの2つ分定義しています。
syntax = "proto3";
package helloworld;
import "google/api/annotations.proto";
service Greeter {
rpc GetTest(HelloRequest) returns (HelloResponse) {
option (google.api.http) = {
get: "/hello"
};
}
rpc PostTest(HelloRequest) returns (HelloResponse) {
option (google.api.http) = {
post: "/hello"
body: "*"
};
}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
次にgRPCサーバのDockerfile
です。alpineをベースイメージにして必要となるものをインストールしています。
FROM alpine:3.8
RUN apk add --no-cache curl git go unzip musl-dev libc6-compat
ENV PROTOBUF_VERSION 3.11.4
RUN curl -sL https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip -o /tmp/protoc.zip && \
unzip /tmp/protoc.zip -d /tmp && \
cp -r /tmp/bin /tmp/include /usr/local/ && \
rm -rf /tmp/*
ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin:/usr/local/go/bin
RUN mkdir /go && \
go get -u google.golang.org/grpc && \
go get -u github.com/golang/protobuf/protoc-gen-go && \
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway && \
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
main.go
ではprotoファイルに書かれた内容を元にGETとPOSTの関数を定義します。
GETのときにはHello World
、POSTのときにはHello <NAME>
となるようにしました。
実行には次章で後述するpb.goファイルが必要となります。
package main
import (
"log"
"net"
pb "pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
type server struct{}
func (s *server) GetTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello World"}, nil
}
func (s *server) PostTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
最後にenvoyの設定ファイルenvoy.yaml
です。
コンテナ間通信のため、host名(address)は宛先のコンテナ名であるgrpc-server
を指定しています。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener1
address:
socket_address: { address: 0.0.0.0, port_value: 51051 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
stat_prefix: grpc_json
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: grpc, timeout: { seconds: 60 } }
http_filters:
- name: envoy.grpc_json_transcoder
config:
proto_descriptor: "/etc/pb/helloworld.pb"
services: ["helloworld.Greeter"]
print_options:
add_whitespace: true
always_print_primitive_fields: true
always_print_enums_as_ints: false
preserve_proto_field_names: false
- name: envoy.router
clusters:
- name: grpc
connect_timeout: 1.25s
type: logical_dns
lb_policy: round_robin
dns_lookup_family: V4_ONLY
http2_protocol_options: {}
load_assignment:
cluster_name: grpc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: grpc-server
port_value: 50051
サーバ設定
envoyとgRPCサーバを起動する前にpbファイル作成とgolangファイルビルドを行います。
最終的なフォルダ構成は次のようになります。(*)が付いたファイルが新たに作成されるものです。
├── docker-compose.yaml
├── pb
│ ├── helloworld.proto
│ ├── helloworld.pb (*)
│ └── helloworld.pb.go (*)
├── server
│ ├── Dockerfile
│ ├── main.go
│ └── bin
│ └── server (*)
└── envoy
└── envoy.yaml
まずはgRPCサーバをコンテナとして起動します。
$ docker-compose run --rm server /bin/sh
protoファイルからpbファイルを作成します。これはenvoyで使用されるバイナリファイルです。
# protoc \
-I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
-I /go/src/pb/ \
--include_imports \
--include_source_info \
--descriptor_set_out=/go/src/pb/helloworld.pb \
helloworld.proto
protoファイルからpb.goファイルを作成します。これはgRPCサーバで使用されます。
# protoc \
-I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--proto_path /go/src/pb \
--go_out=plugins=grpc:/go/src/pb \
helloworld.proto
次にgo buildを行いサーバ起動用のバイナリファイルを作成します。
# cd /go/src/server && go build -i -v -o bin/server
exitします。コンテナ起動時のオプションに--rm
をつけていたためコンテナは自動で削除されます。
# exit
サーバ起動
docker-composeでenvoyとgRPCサーバ両方を起動します。
$ docker-compose up -d
確認
まずはGETリクエストをenvoyに対して送ります。Hello World
がレスポンスとして返ってくることが確認できます。
$ curl http://localhost:51051/hello -v
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 51051 (#0)
> GET /hello HTTP/1.1
> Host: localhost:51051
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json
< x-envoy-upstream-service-time: 8
< grpc-status: 0
< grpc-message:
< content-length: 30
< date: Sat, 14 Mar 2020 07:31:18 GMT
< server: envoy
<
{
"message": "Hello World"
}
* Connection #0 to host localhost left intact
次にPOSTリクエストを送ります。リクエストボディに含めた{"name":"samskeyti88"}
がレスポンスに反映されることが確認できます。
$ curl -X POST http://localhost:51051/hello -d '{"name":"samskeyti88"}' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 51051 (#0)
> POST /hello HTTP/1.1
> Host: localhost:51051
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Length: 22
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 22 out of 22 bytes
< HTTP/1.1 200 OK
< content-type: application/json
< x-envoy-upstream-service-time: 0
< grpc-status: 0
< grpc-message:
< content-length: 36
< date: Sat, 14 Mar 2020 07:50:14 GMT
< server: envoy
<
{
"message": "Hello samskeyti88"
}
* Connection #0 to host localhost left intact
おわりに
envoyのgRPC-JSON transcoderを使用してHTTPリクエストをgRPCに変換できることをDockerコンテナを用いて確認しました。
自分でこの変換を実装するよりははるかに楽にできることが分かります。
参考
gRPC-JSON transcoder
How to build a REST API with gRPC and get the best of two worlds
JimmyCYJ/grpc-transcoding-experiment