パパエンジニアのポエム

奥さんと娘ちゃんへの愛が止まらない

Golangでgrpc-gateway使ってみた

前回の記事gRPCを使ったので、今回はgrpc-gatewayを使ってみる。

grpc-gateway とは

RESTfulなJSON APIをgRPCに変換するリバースプロキシを生成してくれるprotocのプラグイン

https://camo.githubusercontent.com/e75a8b46b078a3c1df0ed9966a16c24add9ccb83/68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f3132687034435071724e5046686174744c5f63496f4a707446766c41716d35774c513067677149356d6b43672f7075623f773d37343926683d333730

RESTfulなJSON APIのバックエンドでgRPCを使いたいという案件に最適。
ドキュメント見ながらミニマムで実装してみる。
Usage | grpc-gateway

登場人物

1つのgateway(リバースプロキシサーバ) に、2つのservice(gRPCサーバ)がぶら下がっている構成で組む。

gateway/main.go

  • ポート8080
  • RESTfulなJSON APIで受けて、gRPCで各サービスと通信を行うリバースプロキシサーバ

echo/main.go

  • ポート9090
  • gRPCで受けたパラメータをそのまま返すサーバ

auth/main.go

  • ポート9091
  • Authorizationヘッダが入ってないとエラーをレスポンス
  • gRPCで受けたパラメータをそのまま返すサーバ

.protoから.bp.go.bp.gw.goを生成する

.protoから、gRPCのインターフェースを定義した.bp.goを生成するコマンド。

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  proto/*.proto

.protoから、リバースプロキシとして動作するためのインターフェースを定義した.bp.gw.go生成

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  proto/*.proto

ちなみに、swaggerも吐き出せるので便利。

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --swagger_out=logtostderr=true:. \
  proto/*.proto

リバースプロキシサーバ実装

echoとauthの2つのgRPCサーバへのプロキシサーバを

grpc-ecosystem.github.io
ここを見ながらリクエストをパイプランするmatcherfilterも実装してみた、特に意味はない。

ポート9090、9091へプロキシ

package main

import (
    "flag"
    "fmt"
    "net/http"

    "github.com/golang/glog"
    "github.com/golang/protobuf/proto"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"

    gw "github.com/yuki-toida/grpc-gateway-sample/proto"
)

func main() {
    flag.Parse()
    defer glog.Flush()

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    mux := runtime.NewServeMux(runtime.WithIncomingHeaderMatcher(matcher), runtime.WithForwardResponseOption(filter))
    opts := []grpc.DialOption{grpc.WithInsecure()}

    if err := gw.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts); err != nil {
        panic(err)
    }

    if err := gw.RegisterAuthServiceHandlerFromEndpoint(ctx, mux, "localhost:9091", opts); err != nil {
        panic(err)
    }

    if err := http.ListenAndServe(":8080", mux); err != nil {
        glog.Fatal(err)
    }

}

func matcher(headerName string) (string, bool) {
    ok := headerName != "Ignore"
    return headerName, ok
}

func filter(ctx context.Context, w http.ResponseWriter, resp proto.Message) error {
    w.Header().Set("X-Filter", "FilterValue")
    return nil
}

echo/main.go サーバ実装

何の変哲もないgRPCサーバ、ポート9090でリッスン

package main

import (
    "context"
    "net"

    pb "github.com/yuki-toida/grpc-gateway-sample/proto"
    "google.golang.org/grpc"
)

func main() {
    listener, err := net.Listen("tcp", ":9090")
    if err != nil {
        panic(err)
    }

    server := grpc.NewServer()
    pb.RegisterEchoServiceServer(server, &EchoServiceServer{})
    server.Serve(listener)
}

type EchoServiceServer struct{}

func (s *EchoServiceServer) Post(c context.Context, m *pb.Message) (*pb.Message, error) {
    return m, nil
}

func (s *EchoServiceServer) Get(c context.Context, p *pb.Param) (*pb.Param, error) {
    return p, nil
}

auth/main.go サーバ実装

grpc.UnaryInterceptor(authentication)でHTTPヘッダーにAuthorizationの有無を判定している
ポート9091でリッスン

package main

import (
    "context"
    "fmt"
    "net"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    pb "github.com/yuki-toida/grpc-gateway-sample/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
)

func main() {
    listener, err := net.Listen("tcp", ":9091")
    if err != nil {
        panic(err)
    }

    opts := []grpc.ServerOption{grpc.UnaryInterceptor(authentication)}
    server := grpc.NewServer(opts...)

    pb.RegisterAuthServiceServer(server, &AuthServiceServer{})
    server.Serve(listener)
}

func authentication(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "not found metadata")
    }
    values := md["authorization"]
    if len(values) == 0 {
        return nil, status.Error(codes.Unauthenticated, "not found metadata")
    }
    return handler(ctx, req)
}

type AuthServiceServer struct{}

func (s *AuthServiceServer) Get(c context.Context, p *pb.Param) (*pb.Param, error) {
    return p, nil
}

動作確認

  • echoサーバ f:id:yuki-toida:20181113150712p:plain

  • authサーバ (authorizationヘッダ無) f:id:yuki-toida:20181113150845p:plain

  • authサーバ (authorizationヘッダ有) f:id:yuki-toida:20181113151100p:plain

動いてそうだ、これを実際にプロダクションで使うとなるとモノリスからマイクロサービスになるだろうし、.protoファイルどこに置くとか、サービス間の通信部分の実装どこに書くとか、色々複雑になってきそうだけど、やりきれたら楽しそうな気配がある。
GitHub - yuki-toida/grpc-gateway-sample