※この記事は、 CyberAgent PTA Advent Calendar 2020の21日目の記事です。
現在サイバーエージェントのメディア事業部でエンジニアをやっているwata3110です。
この記事は、gRPC-Web Hello World Guideを参考に手を動かした時のことをまとめました。
はじめにざっくりとgRPC-webとは何かを説明したあと、触ってみてハマったところなどを書いていきたいと思います。
まだ理解できていないところも多いため訂正や補足ありましたら、コメントいただけると幸いです。
gRPC-webとは
webクライアントがHTTPサーバーを経由することなく、gRPCバックエンドサービスと直接通信できるようにするJavaScriptクライアントライブラリです。
クライアント側とサーバー側のデータ型とサービスインターフェイスをProtocol Bufferで定義することができ、end-to-endのgRPCアプリケーションアーキテクチャを簡単に構築できることがgRPC-Webを使用することの大きな利点の一つになります。
クライアントはバックエンドに対して直接呼び出すことはできず、クライアント呼び出しをgRPCに適した呼び出しに変換する必要があり、その役割はEnvoyがクライアントによって生成されたHTTP/1.1呼び出しをバックエンドのサービスで処理できるHTTP/2呼び出しに変換することで実現しています。
触ってみる
Protocol Bufferを定義する
HelloRequestを渡すとHelloResponseを返すrpcメソッドSayHelloをもつサービスGreeterを定義します。
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
##サービスを実装する
クライアントからリクエストを受け取り、
それを元にレスポンスを作成してクライアントに送り返します。
var PROTO_PATH = __dirname + '/helloworld.proto';
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
var protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
var helloworld = protoDescriptor.helloworld;
function doSayHello(call, callback) {
callback(null, {
message: 'Hello! ' + call.request.name
});
}
function getServer() {
var server = new grpc.Server();
server.addService(helloworld.Greeter.service, {
sayHello: doSayHello,
});
return server;
}
if (require.main === module) {
var server = getServer();
server.bindAsync(
'0.0.0.0:9090', grpc.ServerCredentials.createInsecure(), (err, port) => {
assert.ifError(err);
server.start();
});
}
exports.getServer = getServer;
##クライアントコードを書く
クライアント用のjsファイルを以下コマンドで生成します。
import_styleにtypescriptを指定すれば、typescriptで出力できます。
$protoc -I=. helloworld.proto \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
生成したファイルを元にclient.jsを実装します。
const {HelloRequest, HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');
var client = new GreeterClient('http://localhost:8080');
var request = new HelloRequest();
request.setName('World');
client.sayHello(request, {}, (err, response) => {
console.log(response.getMessage());
});
##package.json
package.jsonを作成します。
{
"name": "grpc-web-simple-example",
"version": "0.1.0",
"description": "gRPC-Web simple example",
"main": "server.js",
"devDependencies": {
"@grpc/grpc-js": "~1.0.5",
"@grpc/proto-loader": "~0.5.4",
"async": "~1.5.2",
"google-protobuf": "~3.14.0",
"grpc-web": "~1.2.1",
"lodash": "~4.17.0",
"webpack": "~4.43.0",
"webpack-cli": "~3.3.11"
},
"scripts": {
"build": "webpack client.js --mode development"
}
}
##index.html
簡単な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の設定ファイルを記述します。
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: 8080 }
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
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
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 9090
###クライアント用のjsコードをコンパイルする
npm install
npm run build
##動かしてみる
gRPCサービスを立ち上げます。
$ node server.js
次にEnvoyを立ち上げます。
上記のenvoy.yml
でポート:8080でブラウザからのリクエストを受け取り、ポート:9090に転送するようにEnvoyを構成しています。
docker run -d -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml:ro \
-p 8080:8080 -p 9901:9901 envoyproxy/envoy:v1.16.1
webサーバーを立ち上げます
python3 -m http.server 8081
全て正常に起動させた後、localhost:8081にアクセスすると、
devツールのconsoleにhello world
が表示されます。
##ハマったところ
gRPCサービス、Envoy、webサーバーそれぞれ起動はしたが、consoleに何も表示されない事象が発生した。
##原因
↓元のコード
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
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.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: greeter_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}] // ←ここを修正
ガイドに沿って書いた元のenvoy.yml
のコードはclustersのhostsフィールドが使われてました。
Envoy documentationを見ると、hostsは非推奨になっており、代わりにload_assginmentを使うようにと書いてあったのでその通りにしたら正常にHello Wolrd
が表示されるようになりました。
##まとめ
ただHello Worldするだけでしたが、なかなかうまく行かない部分が多く苦労しました。
それでもなんとなくですがgRPC-webの概要は理解できるようになったと思います。
次は何か作ってより理解を深めたいと思います。
##参考文献