Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What is going on with this article?
@wata3110

gRPC-webを触ってみた

※この記事は、 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の概要は理解できるようになったと思います。
次は何か作ってより理解を深めたいと思います。

参考文献

0
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
wata3110

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
0
Help us understand the problem. What is going on with this article?