gRPC-Web のリクエストを Envoy で Proxy する
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 の定義は以下とします。
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 を実装します。
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 を実装します。
<!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 するようにします。
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 で動かすための準備をしていきます。
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 です。
version: '3'
services:
envoy:
build:
context: ./proxy
container_name: envoy-grpc-proxy
ports:
- 9000:9000
各種起動コマンドの準備
gRPC Server, gRPC-Web Client の起動コマンドを 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 で書き直したらうまく行きました。
参考
- https://developers.google.com/protocol-buffers/docs/reference/go-generated
- https://grpc.io/docs/languages/go/quickstart/
- https://github.com/grpc/grpc-go/tree/master/examples
- https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld
-
node, go, protoc のセットアップが完了していることを前提としています。 ↩︎
Discussion