前書き
今回の記事ではgRPCについての基礎的な説明とProtocol Buffersの記述方法にはあまり触れません。以前にGo言語+gRPCをはじめから丁寧に解説してみた[ハンズオン]という記事を作成したのでそちらを踏まえた上で本記事を閲覧してもらえると助かります。
この記事の内容
今回はGolangにおいてgRPCサーバにREST形式でアクセスしたい時のアプローチについて説明します。
この記事を読むことにより、
- gRPC-Gatewayを使ってアクセスする方法
- EnvoyのgRPC-JSON-transcoderフィルタを使ってアクセスする方法
以上二つのアプローチの概観を理解することができます。
早速取り掛かりましょう!
gRPC-Gateway
アーキテクチャ図
上図を見ると、アクセスしたいgRPCサービスにリバースプロキシ的な役割を持つサーバーをgrpc-gatewayが作成することでAPIクライアントからREST形式で繋げるようにしています。
また、あくまで左側のprotoファイルからスタブを生成していることがわかります。
実装
今回は私が作成した前回の記事で使用したサンプルリポジトリをベースに変更していきます。
サンプルリポジトリ ⬇︎
また、完成系は別途リポジトリを分けて作成しており、以下のリポジトリのgrpc-gatewayブランチを参考にしていただけると幸いです。
フォルダ構成
今回のフォルダ構成は以下になっています。
.
└── grpc_tutorial
├── Makefile
├── chat
│ ├── chat.go
│ ├── chat.pb.go
│ ├── chat.pb.gw.go
│ └── chat_grpc.pb.go
├── chat.proto
├── cmd
│ ├── gateway
│ │ └── main.go
│ └── server
│ └── main.go
├── go.mod
└── go.sum
5 directories, 10 files
プラグインのインストール
$ go get -u -v github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
$ go get -u -v github.com/golang/protobuf/protoc-gen-go
今回インストールはこれで終了です。
Makefileの作成
protocコマンドをが長くて見にくいので筆者はよくMakefileにコマンドを貼り付けています。
protoc:
protoc -I/usr/local/include -I.\
-I/usr/local/opt/protobuf/include \
-I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis
--plugin=protoc-gen-grpc-gateway=$(GOPATH)/bin/protoc-gen-grpc-gateway --grpc-gateway_out=logtostderr=true:./chat ./chat.proto \
&& protoc -I/usr/local/include -I. \
-I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
-I/usr/local/opt/protobuf/include \
--go_out=./chat --go-grpc_out=./chat ./chat.proto
こちら筆者が行った時はv1.16.0だったのですが、違う場合があります。その場合は自分のバージョンを調べて適宜修正ください。(恐らくmodule not found が出ると思われます。)
コマンドの実行
$ make protoc
こちらのコマンドでchatディレクトリ内にchat_grpc.pb.go, chat_pb.go, chat.pb.gw.goが生成されたかと思います。もしエラーが発生するようでしたら、フォルダ構成の見直し、gopathの見直しを行ってください。
chat.protoの編集
chat.proto
syntax = "proto3";
package chat;
import "google/api/annotations.proto";
service ChatService {
rpc SayHello(MessageRequest) returns (MessageResponse) {
option (google.api.http) = {
get: "/hello"
};
}
}
message MessageRequest {
string body = 1;
}
message MessageResponse {
string body = 1;
}
こちらはgatewayとは関係ないですが、一般的にリクエストとレスポンスの型を作成することが多いため、変更しました。
import "google/api/annotations.proto";
option (google.api.http) = {
get: "/hello"
};
gatewayに関わるコードは上記です。詳細の動きを追いたい方はこちらをご覧ください
gatewayサーバーの実装
gateway/main.go
package main
import (
"flag"
"fmt"
"net/http"
"github.com/golang/glog"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"golang.org/x/net/context"
"google.golang.org/grpc"
"grpc-intro/chat"
)
func run() error {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
endpoint := fmt.Sprintf("localhost:9000")
err := chat.RegisterChatServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)
if err != nil {
return err
}
return http.ListenAndServe(":5000", mux)
}
func main() {
flag.Parse()
defer glog.Flush()
if err := run(); err != nil {
glog.Fatal(err)
}
}
5000ポートでAPIリクエストを受け取り、9000ポートのgrpcサーバーに渡す処理を書いています。
err := chat.RegisterChatServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)
この部分が定義したサービスによって変わってきます。
実装完了です!
試してみます。
go run cmd/server/main.go
//Go gRPC Beginners Tutorial!
// 別ターミナル
go run cmd/gateway/main.go
//別ターミナル
curl localhost:5000/hello
// {"body":"Hello From the Server!"}
動作確認成功です!
これにてgrpc-gatewayの基本的な使い方は完了です!
Envoy(gRPC-JSON-transcoder)
さて、二つ目のアプローチです。こちらはEnvoyのgRPC-JSON-transcoderというHTTPフィルターを使用します。
↓完成形です。
先ほどのgrpc-gatewayのブランチと比較した変更点はこちらになっています。
フォルダ構造を表示します。
.
├── Makefile
├── chat
│ ├── chat.go
│ ├── chat.pb.go
│ └── chat_grpc.pb.go
├── chat.proto
├── cmd
│ └── server
│ └── main.go
├── docker-compose.yaml
├── envoy
│ ├── envoy.yaml
│ └── proto.pb
├── go.mod
└── go.sum
4 directories, 11 files
大きく異なる点はenvoyフォルダがあることと、gatewayフォルダがなくなっていることですね。
それでは変更ファイルをみていきます。
Makefile
Makefile
envoy_protoc:
protoc -I/usr/local/include -I. -I/usr/local/opt/protobuf/include -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
--include_imports \
--include_source_info \
--descriptor_set_out=./envoy/proto.pb \
./chat.proto
envoyが読み取るためのproto.pbバイナリを作成してenvoyフォルダに設置します。
バージョンが違う恐れがあるので適宜修正ください。
$ make envoy_protoc
ここでエラーが発生したらバージョン、gopathを確認お願いします。
docker-compose.yaml
docker-compose.yaml
version: "3"
services:
envoy:
image: envoyproxy/envoy:v1.13.1
volumes:
- "./envoy:/etc/envoy"
expose:
- 51051
ports:
- 51051:51051
envoyのイメージを使います。51051ポートがデフォルトのようなので今回はそのままにしました。
volumesでenvoyフォルダをマウントしています。
envoy.yaml
envoy/envoy.yaml
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener1
address:
socket_address: { address: 0.0.0.0, port_value: 51051 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
stat_prefix: grpc_json
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
# NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path.
# Reference: https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
- match: { prefix: "/" }
route: { cluster: grpc, timeout: { seconds: 60 } }
http_filters:
- name: envoy.grpc_json_transcoder
config:
proto_descriptor: "/etc/envoy/proto.pb"
services: ["chat.ChatService"]
print_options:
add_whitespace: true
always_print_primitive_fields: true
always_print_enums_as_ints: false
preserve_proto_field_names: false
- name: envoy.router
clusters:
- name: grpc
connect_timeout: 1.25s
type: logical_dns
lb_policy: round_robin
dns_lookup_family: V4_ONLY
http2_protocol_options: {}
load_assignment:
cluster_name: grpc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# WARNING: "docker.for.mac.localhost" has been deprecated from Docker v18.03.0.
# If you're running an older version of Docker, please use "docker.for.mac.localhost" instead.
# Reference: https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18030-ce-mac59-2018-03-26
address: 192.168.xxx.xxx
port_value: 50051
この設定は公式の設定からほぼ変えていません。
proto_descriptor: "/etc/envoy/proto.pb"
services: ["chat.ChatService"]
こちらの部分と、アドレス部分を変更しました。
address: 192.168.xxx.xxx
port_value: 50051
addressは今回goをローカル環境で動かしているため、自分のpcのアドレスを指定してあげる必要があります。
ipconfigなどのコマンドを打てばわかるので各自確認して追加してください。
サーバーポートの変更
envoyの設定を変えればいいのですが、server.goを一行編集すればいいかと思い、ここは楽をしてしまいました。
package main
import (
"fmt"
"log"
"net"
"grpc-intro/chat"
"google.golang.org/grpc"
)
func main() {
fmt.Println("Go gRPC Beginners Tutorial!")
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 51051)) // ここだけ変える
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := chat.Server{}
grpcServer := grpc.NewServer()
chat.RegisterChatServiceServer(grpcServer, &s)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %s", err)
}
}
これでポートが変わりました。
実行
$ docker-compose up -d
$ go run cmd/server/main.go
// ターミナル変える
$ curl localhost:51051/hello
// {"body":"Hello From the Server!"}
完成です!
最後に
ここまででgRPCとRESTをつなげる方法を二つ紹介しました。grpc-gatewayはgolangのみのサポートですが,envoyを使った方法は異なる言語でも活用できるので柔軟です。
ZOZOの導入事例の記事を引用すると
grpc-gatewayがあります。こちらもHTTP JSON APIリクエストをgRPCに変換して背後にあるgRPCサーバーへプロキシするものです。以下の図にgrpc-gatewayを利用する構成の例を示します。gRPCとHTTP JSON APIでリクエストの経路を分離する場合やEnvoyではないコンポーネントを使う場合は良いかもしれません。一方ZOZOMATシステムの場合、両者とも同じ経路となるためEnvoyを利用、かつgRPC-JSON transcoderを有効にした構成となっています。
と書かれています。Envoyを使っている場合はそのままフィルタ機能を使った方が良さそうです。
もしもこの記事に不明瞭な点がありましたら気軽にコメントをお願いします。
ここまで読んでくださりありがとうございました!
参考文献