gRPC-Gateway in the production environment (Part 1)

Butter
4 min readJan 30, 2020

--

I recently replaced the echo framework with the gRPC-Gateway.

This article is divided into part one and part two.
I will also use sample code.

What is gRPC-Gateway?

gRPC-Gateway is middleware for converting APIs written in gRPC into JSON over HTTP APIs and providing them.

Install

$ 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
$ # fetches this repo into $GOPATH
$ go get -d github.com/envoyproxy/protoc-gen-validate
$ # install PGV into $GOPATH/bin
$ make build

Proto files

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 {
}

Generate Go files

$ 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

When you run this command, login.pb.go and login.pb.validate.go should be generated.

Implement gRPC Server

From here, I’m going to walk you through the actual code.
If you want to see the whole code, take a look at the sample code.

app/cmd/server/main.go

package main

// Omission...

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)
s.GracefulStop()
}

In the above code, the gRPC server is started on port 9090.
(The DB part is omitted because it is not necessary to explain it.)

newGRPCServer will return *grpc.Server and use it to start the server on any port.
Let’s look at the implementation of 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
}

In the above code defines four types of interceptors and sets them to be used by login and user.
RegisterLoginServiceServer and RegisterUserServiceServer are implemented in the go file generated by the above command, so just call it.
In addition, interfaces such as LoginServiceServer and UserServiceServer are defined in the generated go file.
NewLoginServiceServer and NewUserServiceServer are functions that initialize each of them after implementing it.

Although it has been described on various commentary pages so far, there is little information on interceptors and it is necessary to select the interface necessary for the project to be introduced and implement it in some cases, so the following 4 learn about the different types of interceptors.

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
}

The above interceptor is a code that gets metadata from context, returns the value of x-request-id if there is a key, and returns <unknown> otherwise.
I will explain the shortage in the second part, but use metadata to link information such as HTTP Request Header with gRPC Client.

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)
}
}

The above interceptor is the code that executes the authentication process in the service that implements the Authenticate interface.
Although it is very simple, it is possible to implement authentication processing for each service.

grpc_validator.UnaryServerInterceptor

This is an interceptor at github.com/grpc-ecosystem/go-grpc-middleware/validator.
The *.pb.validate.go generated by the go file generation command with the--validate_out option is validated.
If you use validate in your proto file, be sure to use this interface.

grpc_recovery.UnaryServerInterceptor

This is an interceptor at github.com/grpc-ecosystem/go-grpc-middleware/recovery that handles panic.

ChainUnaryServer

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

Use grpc_middleware.ChainUnaryServer to run the previous four interceptors in order.

Conclusion

This completes the description of the gRPC Server implementation.
It is important to note that this is a description of the gRPC Server, not a gRPC Client (gRPC-Gateway).
Also, as you may have noticed in the code, the interceptor introduced is for gRPC Server.
In the second part, I will explain the gRPC Client interceptor (log, request ID numbering, etc.), so stay tuned.

--

--

Butter

I’ll be writing articles about the golang in order to become a golang expert. I’ll also aim to improve my English language skills.