🐇

gRPC-Web のリクエストを Envoy で Proxy する

2020/11/02に公開

gRPC-Web のリクエストを gRPC Server へ Proxy する Envoy の設定手順のご紹介です。

サンプルコード

サンプルコードを用意しましたのでよろしければお手元で試しながら読んでみてください。[1]

ディレクトリ構成

ディレクトリの構成は下記のようになっています。

./
├── Makefile # 各種コマンド
├── client # gRPC-Web Client 関連
│   ├── client.js
│   ├── dist
│   ├── index.html
│   └── generated
│       └── helloworld
│           ├── helloworld_grpc_web_pb.js
│           └── helloworld_pb.js
├── docker-compose.yml # Envoy の起動設定
├── proto # protobuf
│   └── helloworld
│       └── helloworld.proto
├── proxy # Envoy 関連
│   ├── Dockerfile
│   └── envoy.yaml
└── server # gRPC Server 関連
    ├── go.mod
    ├── go.sum
    └── main.go
    └── generated
        └── helloworld
            ├── helloworld.pb.go
            └── helloworld_grpc.pb.go

protobuf の定義

protobuf の定義は以下とします。

proto/helloworld/helloworld.proto
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

gRPC Server の準備

上記の proto に対応した gRPC Server を準備します。 今回は gRPC のドキュメントにある QuickStart を参考に実装しましたが詳細は割愛します。

cd server
go run main.go
evans -r --host localhost -p 50051

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

 more expressive universal gRPC client


helloworld.Greeter@localhost:50051> call SayHello
name (TYPE_STRING) => Envoy
{
  "message": "Hello Envoy"
}

gRPC-Web Client の準備

JavaScript で gRPC-Web Client を実装します。

コード自動生成

proto に対応するクライアントコードを自動生成します。

protoc -I=./proto ./proto/helloworld/helloworld.proto \
	--js_out=import_style=commonjs:client/generated \
	--grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/generated

クライアント実装

自動生成したファイルを使って gRPC-Web Client を実装します。

client/client.js
const {HelloRequest} = require('./generated/helloworld/helloworld_pb.js');
const {GreeterClient} = require('./generated/helloworld/helloworld_grpc_web_pb.js');

const client = new GreeterClient('http://localhost:9000', null, null);

const request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
    if (err) {
        console.log(`Unexpected error for sayHello: code = ${err.code}` +
            `, message = "${err.message}"`);
    } else {
        console.log(response.getMessage());
    }
});

また、上記の実装コードをブラウザから呼び出すため簡単な HTML を実装します。

client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>gRPC-Web Example</title>
    <script src="./dist/main.js"></script>
</head>
<body>
<p>Open up the developer console and see the logs for the output.</p>
</body>
</html>

Envoy の準備

Envoy.yaml

Envoy.yaml に Proxy の設定を記述していきます。

  • Proxy は gRPC-Web からのリクエストを Port 9000 で受け取ります。
  • Port 50051 の gRPC Server に Forward するようにします。
proxy/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: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 9000 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
            - name: envoy.access_loggers.file
              typed_config:
                "@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog
                path: "/dev/stdout"
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: greeter_service
                  max_grpc_timeout: 0s
              cors:
                allow_origin_string_match:
                  - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.filters.http.grpc_web
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router
  clusters:
  - name: greeter_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    dns_lookup_family: V4_ONLY
    upstream_connection_options:
      tcp_keepalive:
        keepalive_time: 300
    load_assignment:
      cluster_name: cluster_0
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: host.docker.internal
                    port_value: 50051

Dockerの設定

Envoy を Docker で動かすための準備をしていきます。

proxy/Dockerfile
FROM envoyproxy/envoy:v1.15.0
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

続いて、docker-compose.yaml です。

docker-compose.yaml
version: '3'
services:
  envoy:
    build:
      context: ./proxy
    container_name: envoy-grpc-proxy
    ports:
      - 9000:9000

各種起動コマンドの準備

gRPC Server, gRPC-Web Client の起動コマンドを Makefile にまとめると以下のようになります。

Makefile
server:
	cd ./server && go run main.go

client:
	cd ./client && \
		npx webpack --mode=development client.js && \
		yarn static -p 8081

.PHONY: server client

これで準備が整いました。

動作確認

実際に動かしてみます。
下記のコマンドをそれぞれターミナルウィンドウを開いて実行します。

# Start gRPC Server
make server

# Start gRPC-Web Client
make client

# Start Envoy
docker-compose up

ブラウザから http://localhost:8081/ にアクセスしてみると gRPC Server にリクエストされていることがわかります。

また、Envoy のログにも受け取ったリクエスト情報の出力が確認できます。

Envoy によってブラウザからのリクエストが gRPC Server に Forward されていることが確認できました。

ハマったところ

はじめに gRPC-Web の github のサンプルコード で同様の手順を試していたのですが、JavaScript で実装された gRPC Server がうまく動かなかったり Envoy.yaml の記述が古かったりしたので原因調査にかなり時間を消費しました😢

結局、あちこちドキュメントを漁って Envoy.yaml を記述を改め、gRPC Server を Go で書き直したらうまく行きました。

参考

脚注
  1. node, go, protoc のセットアップが完了していることを前提としています。 ↩︎

Discussion