gRPC Server On Rails × Envoy × gRPC-Web

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

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

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

### 本記事で説明しないこと
- gRPCについて
- Envoyについて
- Railsについて

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

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

## 構成概要
今回Clientはサーバーを立てないと話しましたが具体的にはgRPC-WEB からサーバーにリクエストを送ります。
### フロー図
![grpc2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/c168553b-e14b-e92a-04e8-c7897eb4bacb.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 だけ返します。
今回、サービスは簡素にいきます。

``` proto: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定義ファイル生成

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

``` shell
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で定義してしまったので、名前がダブって使えませんでした。 ..///


``` ruby: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はこれ以外記述不要です。

``` ruby
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の起動
``` shell
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の起動
``` shell
envoy -c envoy.yaml
```
中身

``` 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定義ファイル生成
``` shell
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に挿入します。

``` js: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
``` shell
npx webpack grpc-client.js
```


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

``` shell
php -S 127.0.0.1:3000
```


## ブラウザでの実行結果
![スクリーンショット 2021-04-02 11.03.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/e67fcc45-76d7-8f4c-0400-bc9278b1153c.png)

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

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