31
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[gRPC-Gateway, Envoy] gRPCとRESTを繋げたい時に...

Last updated at Posted at 2021-03-03

前書き

今回の記事ではgRPCについての基礎的な説明とProtocol Buffersの記述方法にはあまり触れません。以前にGo言語+gRPCをはじめから丁寧に解説してみた[ハンズオン]という記事を作成したのでそちらを踏まえた上で本記事を閲覧してもらえると助かります。

この記事の内容

今回はGolangにおいてgRPCサーバにREST形式でアクセスしたい時のアプローチについて説明します。

この記事を読むことにより、

以上二つのアプローチの概観を理解することができます。

早速取り掛かりましょう!

gRPC-Gateway

アーキテクチャ図

img

#公式より抜粋

上図を見ると、アクセスしたい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)

ITOps Times Open-Source Project of the Week: Envoy - ITOps Times

さて、二つ目のアプローチです。こちらはEnvoygRPC-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を使っている場合はそのままフィルタ機能を使った方が良さそうです。

もしもこの記事に不明瞭な点がありましたら気軽にコメントをお願いします。

ここまで読んでくださりありがとうございました!

参考文献

31
20
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
31
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?