7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

envoyのgRPC-JSON transcoderをローカルで実行する

Last updated at Posted at 2020-03-14

はじめに

HTTP/JSONからgRPCへのtranscodingを行うには大まかに次の2種類の方法があります。

  • gRPC gatewayを使用する。
  • gRPC-JSON transcoderを使用する。

前者はプラグインとして提供されていてHTTPリバースプロキシを作成することになります。これはgolangにしか対応されていません。

後者についてはgrpc-httpjson-transcodingというライブラリがありIstioGoogle cloud endpointで現在も使用されています。
envoy proxy単体でもgRPC serviceにHTTP/JSONインタフェースを適用することで実行可能です。

今回はenvoy, gRPCサーバをDockerコンテナとして立ててgRPC-JSON transcoderのテストを行います。HTTPリクエストはGET/POSTを試しています。

スクリーンショット 2020-03-14 19.18.34.png

コードは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を設定します。

docker-compose.yaml
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つ分定義しています。

pb/helloworld.proto
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をベースイメージにして必要となるものをインストールしています。

server/Dockerfile
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ファイルが必要となります。

server/main.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を指定しています。

envoy/envoy.yaml
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

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?