23
15

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.

VISITSAdvent Calendar 2019

Day 3

本番環境で使えるgRPC-Gateway【前編】

Last updated at Posted at 2019-12-03

VISITS Technologies Advent Calendar 2019 3日目は@istshが担当します。

最近業務で、echoからgRPC-Gatewayに移行することになり、interceptorやmetadataについていろいろ調べたので、
gRPC Serverの実装(前編)gRPC Clientの実装(後編)に分けてお送りしようと思います。

以降の説明は、サンプルコードを使って行います。

gRPC-Gatewayとは

gRPC-Gatewayとは、gRPCで書かれたAPIを、JSON over HTTPのAPIに変換して提供するためのミドルウェアです。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_317035_1efa8277-e25c-8a68-7956-e9460bd4626c.png
(よく見る図)

インストール

$ 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

protoc-gen-validate

$ # fetches this repo into $GOPATH
$ go get -d github.com/envoyproxy/protoc-gen-validate
$ # installs PGV into $GOPATH/bin
$ make build

protoファイル

user.proto
syntax = "proto3";

package v1;
option go_package = "github.com/istsh/go-grpc-sample/app/pb/v1";

import "google/api/annotations.proto";
import "validate/validate.proto";

service UserService {
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
        option (google.api.http) = {
            post: "/v1/user"
            body: "*"
        };
    }
}

message CreateUserRequest {
    string email = 1 [(validate.rules).string = { min_len: 3, max_len: 254 }];
    string password = 2 [(validate.rules).string = { min_len: 8, max_len: 64 }];
}

message CreateUserResponse {
}

goファイルを生成

$ protoc \
    proto/v1/login.proto \
    -I . \
    -I $GOPATH/src/github.com/envoyproxy/protoc-gen-validate \
    -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway \
    --go_out=plugins=grpc:$GOPATH/src \
    --validate_out="lang=go:$GOPATH/src" \
    --grpc-gateway_out=logtostderr=true:$GOPATH/src

これを実行すると、login.pb.gologin.pb.validate.goが生成されるはずです。

gRPC Serverの実装

ここからは実際のコードを見ながら解説していきます。
一部抜粋して説明するので、コード全体が見たい場合はサンプルコードを見てください。

app/cmd/server/main.go
package main

// 省略...

func main() {
	db := connectDB()
	defer db.Close()

	r := persistence.NewDBRepository(db)
	u := usecase.NewUserUsecase()

	listenPort, err := net.Listen("tcp", ":9090")
	if err != nil {
		logrus.Fatalln(err)
	}

	s := newGRPCServer(r, u)
	reflection.Register(s)
	s.Serve(listenPort)
}

上記のコードは、gRPC Serverをポート9090で起動するコードです。
DBの部分は説明する必要はないと思うので割愛します。

newGRPCServer*grpc.Serverを返してくれるので、それを用いて任意のポートでサーバーを起動します。
ではnewGRPCServerの実装を見てみましょう。

app/cmd/server/main.go
func newGRPCServer(r repository.Repository, u usecase.UserUserCase) *grpc.Server {
	s := grpc.NewServer(
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			interceptor.RequestIDInterceptor(),
			interceptor.AuthenticationInterceptor(),
			grpc_validator.UnaryServerInterceptor(),
			grpc_recovery.UnaryServerInterceptor(),
		)),
	)

	pbv1.RegisterLoginServiceServer(s, server.NewLoginServiceServer(r, u))
	pbv1.RegisterUserServiceServer(s, server.NewUserServiceServer(r, u))

	return s
}

上記のコードは、インターセプターを4種類定義し、loginとuserで使うように設定しています。
RegisterLoginServiceServerRegisterUserServiceServerは、前述のコマンドで生成したgoファイルに実装されているので、それを呼び出すだけです。
また、生成されたgoファイルには、LoginServiceServerUserServiceServerといったインターフェースが定義されいます。
それを実装したうえでそれぞれを初期化する関数がNewLoginServiceServerNewUserServiceServerになっています。

ここまではいろんな解説ページに載っていることですが、インターセプターに関しては情報が少ないのと、導入するプロジェクトに必要なインターフェースを選択したり、場合によっては実装することになるので、以降は上記の4種類のインターセプターについて解説します。

RequestIDInterceptor

requestid_interceptor.go
const (
	// XRequestIDKey is a key for getting request id.
	XRequestIDKey    = "X-Request-ID"
	unknownRequestID = "<unknown>"
)

// RequestIDInterceptor is a interceptor of access control list.
func RequestIDInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		requestID := requestIDFromContext(ctx)
		ctx = context.WithValue(ctx, log.CtxRequestIDKey, requestID)
		return handler(ctx, req)
	}
}

func requestIDFromContext(ctx context.Context) string {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return unknownRequestID
	}

	key := strings.ToLower(XRequestIDKey)
	header, ok := md[key]
	if !ok || len(header) == 0 {
		return unknownRequestID
	}

	requestID := header[0]
	if requestID == "" {
		return unknownRequestID
	}

	return requestID
}

上記のインターセプターは、contextからmetadataを取得し、そこにx-request-idというキーがあれば、その値を返し、なければ<unknown>を返すというコードです。
後編で不足分は解説しますが、gRPC ClientとHTTP Request Headerなどの情報を連携する為にmetadataを使います。

AuthenticationInterceptor

authentication_interceptor.go
// Authenticator provides Authenticate method.
// Each service should implement this interface, otherwise, all requests will be rejected with authentication error.
type Authenticator interface {
	Authenticate(ctx context.Context, req interface{}) (context.Context, error)
}

// AuthenticationInterceptor is a interceptor of authentication.
func AuthenticationInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		authenticator, ok := info.Server.(Authenticator)
		if !ok {
			// If Service doesn't implement Authenticator, return InternalServerError always.
			return nil, status.New(codes.Internal, "Authenticator is not implemented").Err()
		}

		ctx, err := authenticator.Authenticate(ctx, req)
		if err != nil {
			return nil, status.New(codes.Unauthenticated, fmt.Sprintf("Not authenticated: %v", err)).Err()
		}

		return handler(ctx, req)
	}
}

上記のインターセプターは、Authenticateインターフェースを実装したサービスで認証の処理を実行するコードです。
非常にシンプルですが、各サービス毎に認証の処理を実装できるようになっています。

grpc_validator.UnaryServerInterceptor

これはgithub.com/grpc-ecosystem/go-grpc-middleware/validatorのインターセプターです。
goファイルの生成コマンドで、--validate_outオプションをつけることで生成される*.pb.validate.goの検証処理をやってくれます。
protoファイルでvalidateを使っている場合はこのインターフェースは必ず使いましょう。

grpc_recovery.UnaryServerInterceptor

これはgithub.com/grpc-ecosystem/go-grpc-middleware/recoveryのインターセプターで、panicをハンドリングしてくれます。

ChainUnaryServer

s := grpc.NewServer(
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        interceptor.RequestIDInterceptor(),
        interceptor.AuthenticationInterceptor(),
        grpc_validator.UnaryServerInterceptor(),
        grpc_recovery.UnaryServerInterceptor(),
    )),
)

これまでの4つのインターセプターを順番に実行するために、grpc_middleware.ChainUnaryServerを使います。

まとめ

これでgRPC Server側の実装は一通り解説しました。
大切なのは、これはgRPC Serverについての説明であって、gRPC Client(gRPC-Gateway)についての説明は別ということです。
また、コードをみて気がついたかもしれませんが、紹介したインターセプターはgRPC Server用のものです。
後編でgRPC Clientのインターセプター(ログ、リクエストIDの採番など)も解説するので、お楽しみに。

23
15
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
23
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?