はじめに
今回、アニメ版ハンターハンターのようなタイトルですね。
突然ですが、いくつか質問をします。
- Railsでマイクロサービスを構築したいと思ったことがありますでしょうか?
- そして、その通信をgRPCで行いたいと思ったことないでしょうか?
- そしてそして、gRPC Clientのサーバーを立つてるとネットワーク増えるから、もしくは面倒だから立てたくないと思ったことはないでしょうか?
え?ある? そんなあなたに朗報です。
この記事を読めばすぐに実現できちゃいます。
本記事で説明しないこと
- gRPCについて
- Envoyについて
- Railsについて
記事のタイトル全部入ってますね。
ある程度理解してから本記事を読むことを推奨します。
今回のゴール
DBからフェッチしたユーザ名をgRPC経由でindex.htmlに表示します。
詳細は構成概要にあります。
構成概要
今回Clientはサーバーを立てないと話しましたが具体的にはgRPC-WEB からサーバーにリクエストを送ります。
フロー図
ディレクトリ構成
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 だけ返します。
今回、サービスは簡素にいきます。
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で定義してしまったので、名前がダブって使えませんでした。 ..///
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
中身
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に挿入します。
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
ブラウザでの実行結果
最後に
さて、本記事の読者の中でビトタケシさん知ってる人、0人説は立証できたでしょうか。
じゃなくてですね。
この記事を読んでなんとなく、フローを理解していただける方やgRPC自体に興味を持ったという方がいたら幸いです。
最終的なソースコードは近日githubに上げたいと思いますので、その時はこの記事を更新します。
ここまで、読んで下さりありがとうございます!