LoginSignup
4
3

More than 3 years have passed since last update.

gRPC Server On Rails × Envoy × gRPC-Web

Last updated at Posted at 2021-04-02

はじめに

今回、アニメ版ハンターハンターのようなタイトルですね。
突然ですが、いくつか質問をします。

  • Railsでマイクロサービスを構築したいと思ったことがありますでしょうか?
  • そして、その通信をgRPCで行いたいと思ったことないでしょうか?
  • そしてそして、gRPC Clientのサーバーを立つてるとネットワーク増えるから、もしくは面倒だから立てたくないと思ったことはないでしょうか?

え?ある? そんなあなたに朗報です。
この記事を読めばすぐに実現できちゃいます。

本記事で説明しないこと

  • gRPCについて
  • Envoyについて
  • Railsについて

記事のタイトル全部入ってますね。
ある程度理解してから本記事を読むことを推奨します。

今回のゴール

DBからフェッチしたユーザ名をgRPC経由でindex.htmlに表示します。
詳細は構成概要にあります。

構成概要

今回Clientはサーバーを立てないと話しましたが具体的にはgRPC-WEB からサーバーにリクエストを送ります。

フロー図

grpc2.png

ディレクトリ構成

grpc-rails % tree -L 2
.
├── Makefile
├── client
│   ├── codegen
│   ├── dist
│   ├── grpc-client.js
│   ├── index.html
│   ├── node_modules
│   ├── package-lock.json
│   └── package.json
├── envoy.yaml
├── rails-api-server
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── README.md
│   ├── Rakefile
│   ├── app
│   ├── bin
│   ├── config
│   ├── config.ru
│   ├── db
│   ├── lib
│   ├── log
│   ├── public
│   ├── storage
│   ├── test
│   ├── tmp
│   └── vendor
└── service
    └── user.proto

gRPC Server On Railsの実装

RailsでどうやってgRPCサーバー立てるの?

rails runnerでサーバー起動します。
当たり前ですけど、rails sで起動するのはhttpサーバーなので。

サービス定義

単純にidをリクエストで受け取ってuser_name だけ返します。
今回、サービスは簡素にいきます。

user.proto
syntax = "proto3";
package user;

service UserInfo {
  rpc Show (UserRequest) returns (UserResponse) {}
}

message UserRequest {
  uint32 id = 1;
}

message UserResponse {
  string user_name = 1;
}

Rubyで使用するProtocol Buffers定義ファイル生成

まずは、サーバーから作っていきます。

grpc_tools_ruby_protoc -I=service user.proto \
 --ruby_out=rails-api-server/lib/grpc_codegen \
 --grpc_out=rails-api-server/lib/grpc_codegen

サービスの処理

シンプルですね。
modelがCustomerなのは、packageをuserで定義してしまったので、名前がダブって使えませんでした。 ..///

app/services/user_info_server.rb
class UserInfoServer < User::UserInfo::Service
  def show(user_req, _unused_call)
    customer = Customer.find(user_req.id)
    User::UserResponse.new(user_name: customer.name)
  end
end

runnerの処理

ここも、シンプルです。
生成したProtobuf定義ファイルはeager_loadで事前にloadしてます。
なので、runnerはこれ以外記述不要です。

class Tasks::RunGrpcServer
  def self.exec
    s = GRPC::RpcServer.new
    s.add_http2_port('0.0.0.0:9090', :this_port_is_insecure)
    s.handle(UserInfoServer)
    s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
  end
end

gRPC Serverの起動

rails runner "Tasks::RunGrpcServer.exec"

テーブル及びデータの生成

model等の生成は解説をスキップしています。
本当はCRUD作りたかったんですけど今回は断念です。
データはseedで入れています。
テーブルの中身はこんな感じです。

mysql> select * from customers \G
*************************** 1. row ***************************
        id: 1
      name: ビトタケシ
created_at: 2021-04-01 23:13:45.019951
updated_at: 2021-04-01 23:13:45.019951
1 row in set (0.01 sec)

Envoyの起動

envoy -c envoy.yaml

中身

envoy.yaml
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.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: echo_service
                  timeout: 0s
                  max_stream_duration:
                    grpc_timeout_header_max: 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: echo_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: 0.0.0.0
                    port_value: 9090
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

gRPC-Webの実装

JSで使用するProtocol Buffers定義ファイル生成

protoc -I=service user.proto \
 --js_out=import_style=commonjs:client/codegen \
 --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/codegen

Client JS

RailsのgRPCサーバへリクエストを投げる処理です。
取得したuser_nameを、HTMLに挿入します。

grpc-client.js
const { UserRequest } = require('./codegen/user_pb.js');
const { UserInfoClient } = require('./codegen/user_grpc_web_pb.js');

const userInfoService = new UserInfoClient('http://0.0.0.0:8080');
const request = new UserRequest();

request.setId(1);
userInfoService.show(request, {}, function(err, response) {
  p = document.getElementById('echo_text');
  p.textContent = response.getUserName();
});

Client JS Build

npx webpack grpc-client.js

http serverの起動

当然ですがindex.htmlの表示はhttpです。
httpdだとDocRootの設定が面倒なので、PHPで起動します。

php -S 127.0.0.1:3000

ブラウザでの実行結果

スクリーンショット 2021-04-02 11.03.39.png

最後に

さて、本記事の読者の中でビトタケシさん知ってる人、0人説は立証できたでしょうか。
じゃなくてですね。
この記事を読んでなんとなく、フローを理解していただける方やgRPC自体に興味を持ったという方がいたら幸いです。
最終的なソースコードは近日githubに上げたいと思いますので、その時はこの記事を更新します。

ここまで、読んで下さりありがとうございます!

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