Writing a Microservice Blog - Part 2 of API Gateway

In the first part of our series of articles “Writing a Blog on Microservices,” we described a general approach to solving the problem.



Now it's the turn of the Gateway API or GW API.



In our c ptimofeev GW API, we implement the following functions:





So let's go ...



To implement the REST / gRPC conversion function, we will use the gosh library grpc-gateway .



Further, in the protofile of each microservice that we want to publish on REST, you need to add an option description in the description section of the service interfaces. It actually specifies the path and method by which REST access will be performed.



//  Category service CategoryService { //  rpc Create (CreateCategoryRequest) returns (CreateCategoryResponse) { option (google.api.http) = { post: "/api/v1/category" }; } }
      
      





Based on this information, the code generation script (./bin/protogen.sh) will create the gRPC server code (in the microservice directory), the gRPC client (in the api-gw directory) and generate the latest API documentation (in the format {{service name}}. swagger.json)



Next, we need to write HTTP Proxy code, which on the one hand will be an HTTP server (for processing REST requests), and on the other hand it will be a gRPC client for our microservices (gRPC servers).



We will place this code in the file ./services/api-gw/main.go.



First, in the import section, we connect client libraries to our microservices

(protogen.sh generated them for us):



 import ( … userService "./services/user/protobuf" postService "./services/post/protobuf" commentService "./services/comment/protobuf" categoryService "./services/category/protobuf" …
      
      





Next, we indicate the addresses and ports on which our gRPC services “hang” (we take the values ​​from the environment variables):



 var ( // gRPC services userServerAdress=fmt.Sprintf("%s:%s",os.Getenv("USER_HOST"),os.Getenv("USER_PORT")) postServerAdress=fmt.Sprintf("%s:%s",os.Getenv("POST_HOST"),os.Getenv("POST_PORT")) commentServerAdress=fmt.Sprintf("%s:%s",os.Getenv("COMMENT_HOST"),os.Getenv("COMMENT_PORT")) categoryServerAdress=fmt.Sprintf("%s:%s",os.Getenv("CATEGORY_HOST"),os.Getenv("CATEGORY_PORT")) )
      
      





And finally, we implement the HTTP Proxy itself:



 func HTTPProxy(proxyAddr string){ grpcGwMux:=runtime.NewServeMux() //---------------------------------------------------------------- //     gRPC //---------------------------------------------------------------- //   User grpcUserConn, err:=grpc.Dial( userServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to User service", err) } defer grpcUserConn.Close() err = userService.RegisterUserServiceHandler( context.Background(), grpcGwMux, grpcUserConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Post grpcPostConn, err:=grpc.Dial( postServerAdress, grpc.WithUnaryInterceptor(AccessLogInterceptor), grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Post service", err) } defer grpcPostConn.Close() err = postService.RegisterPostServiceHandler( context.Background(), grpcGwMux, grpcPostConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Comment grpcCommentConn, err:=grpc.Dial( commentServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Comment service", err) } defer grpcCommentConn.Close() err = commentService.RegisterCommentServiceHandler( context.Background(), grpcGwMux, grpcCommentConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //   Category grpcCategoryConn, err:=grpc.Dial( categoryServerAdress, grpc.WithInsecure(), ) if err!=nil{ log.Fatalln("Failed to connect to Category service", err) } defer grpcCategoryConn.Close() err = categoryService.RegisterCategoryServiceHandler( context.Background(), grpcGwMux, grpcCategoryConn, ) if err!=nil{ log.Fatalln("Failed to start HTTP server", err) } //---------------------------------------------------------------- //     REST //---------------------------------------------------------------- mux:=http.NewServeMux() mux.Handle("/api/v1/",grpcGwMux) mux.HandleFunc("/",helloworld) fmt.Println("starting HTTP server at "+proxyAddr) log.Fatal(http.ListenAndServe(proxyAddr,mux)) }
      
      





In setting up the connection to microservices, we use the grpc.WithUnaryInterceptor (AccessLogInterceptor) option, into which we pass the AccessLogInterceptor function as a parameter. This is nothing more than an implementation of the middleware layer, i.e. the AccessLogInterceptor function will be executed with every gRPC call to the child microservice.



 … //---------------------------------------------------------------- //   Post grpcPostConn, err:=grpc.Dial( … grpc.WithUnaryInterceptor(AccessLogInterceptor), … )
      
      





In turn, in the AccessLogInterceptor function, we already implement authentication, logging, and TraceId generation mechanisms.



If the authorization attribute was specified in the Header in the incoming (REST) ​​request, then we parse and validate it in the CheckGetJWTToken function, which either returns an error or, if successful, returns UserId and UserRole.



 var traceId,userId,userRole string if len(md["authorization"])>0{ tokenString:= md["authorization"][0] if tokenString!=""{ err,token:=userService.CheckGetJWTToken(tokenString) if err!=nil{ return err } userId=fmt.Sprintf("%s",token["UserID"]) userRole=fmt.Sprintf("%s",token["UserRole"]) } }
      
      





Next, we form TraceId and wrap it together with UserId and UserRole in the call context and make the gRPC call of our microservice.



 // ID  traceId=fmt.Sprintf("%d",time.Now().UTC().UnixNano()) callContext:=context.Background() mdOut:=metadata.Pairs( "trace-id",traceId, "user-id",userId, "user-role",userRole, ) callContext=metadata.NewOutgoingContext(callContext,mdOut) err:=invoker(callContext,method,req,reply,cc, opts...)
      
      





And finally, we write a service call event to the log.



 msg:=fmt.Sprintf("Call:%v, traceId: %v, userId: %v, userRole: %v, time: %v", method,traceId,userId,userRole,time.Since(start)) app.AccesLog(msg)
      
      





Another middleware processor is “hanging” on the answers of specific methods (SignIn, SignUp) of the User service. This handler intercepts gRPC responses, picks up the UserID and UserRole response, converts it to JWT Token and gives it (JWT Token) in the REST response as the “Authorization” attribute Header. The middleware code described is implemented on the gRPC client side in the file ./api-gw/services/user/protobuf/functions.go.



We connect the response handler.



 func init() { //     SignIn forward_UserService_SignIn_0 = forwardSignIn //     SignUp forward_UserService_SignUp_0 = forwardSignUp }
      
      





An example is the SignIn response handler (the SignUp handler is similar).



 func forwardSignIn(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message) error) { // proto.Message  SignInResponse signInResponse:=&SignInResponse{} signInResponse.XXX_Merge(resp) token,err:=GetJWTToken(signInResponse.Slug,signInResponse.Role) if err!=nil{ http.Error(w, fmt.Sprintf("%v",err), http.StatusUnauthorized) return } w.Header().Set("authorization", token) runtime.ForwardResponseMessage(ctx, mux, marshaler, w, req, resp, opts...) }
      
      





To be continued…



Yes, the demo of the project can be viewed here , and the source code is here .



All Articles