3
1

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.

gRPC-webを触ってみた

Last updated at Posted at 2020-12-21

※この記事は、 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を使用することの大きな利点の一つになります。1_mAkZWyRD9gKyBEOaqEFm-A.png
クライアントはバックエンドに対して直接呼び出すことはできず、クライアント呼び出しをgRPCに適した呼び出しに変換する必要があり、その役割はEnvoyがクライアントによって生成されたHTTP/1.1呼び出しをバックエンドのサービスで処理できるHTTP/2呼び出しに変換することで実現しています。

触ってみる

Protocol Bufferを定義する

HelloRequestを渡すとHelloResponseを返すrpcメソッドSayHelloをもつサービスGreeterを定義します。

helloworld.proto
syntax = "proto3";

package helloworld;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

##サービスを実装する
クライアントからリクエストを受け取り、
それを元にレスポンスを作成してクライアントに送り返します。

server.js
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を実装します。

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を作成します。

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を実装します。

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.yml
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が表示されます。

スクリーンショット 2020-12-21 8.12.04.png

##ハマったところ

gRPCサービス、Envoy、webサーバーそれぞれ起動はしたが、consoleに何も表示されない事象が発生した。

##原因

↓元のコード

envoy.yml
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が表示されるようになりました。

スクリーンショット 2020-12-21 8.53.23.png

##まとめ
ただHello Worldするだけでしたが、なかなかうまく行かない部分が多く苦労しました。
それでもなんとなくですがgRPC-webの概要は理解できるようになったと思います。
次は何か作ってより理解を深めたいと思います。

##参考文献

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?