В первой части нашего цикла статей «Пишем блог на микросервисах» мы описали общий подход к решению задачи.
Теперь пришла очередь API Gateway или API GW.
В нашем c ptimofeev API GW мы реализуем следующие функции:
Итак, поехали…
Для реализации функции конвертация REST/gRPC будем использовать гошную библиотеку grpc-gateway.
Далее в протофайле каждого микросервиса, который хотим опубликовать по REST, необходимо добавить описание option в секции описания интерфейсов сервиса. Здесь собственно указывается путь и метод, по которому будет осуществляться доступ по REST.
На основе этой информации скрипт генерации кода (./bin/protogen.sh) создаст код gRPC сервера (в каталоге микросервиса), gRPC клиента (в каталоге api-gw) и сгенерирует актуальную документацию API (в формате {{имя сервиса}}.swagger.json)
Далее нам нужно написать код HTTP Proxy, который с одной стороны будет HTTP сервером (для обработки REST запросов), а с другой стороны будет gRPC клиентом для наших микросервис��в (gRPC серверов).
Этот код мы разместим в файле ./services/api-gw/main.go.
В начале в секции import подключаем клиентские библиотеки к наших микросервисов
(их нам сгенерил protogen.sh):
Далее указываем адреса и порты на которых «висят» наши gRPC сервисы (значения берем из переменных окружений):
И, наконец, реализуем сам HTTP Proxy:
В настройке подключения к микросервисам мы используем опцию grpc.WithUnaryInterceptor(AccessLogInterceptor), в которую в качестве параметра передаем функцию AccessLogInterceptor. Это не что иное как реализация middleware слоя, т.е. функция AccessLogInterceptor будет выполняться при каждом gRPC вызове дочернего микросервиса.
В свою очередь, в функции AccessLogInterceptor мы уже реализуем механизмы аутентификации, логирования и генерации TraceId.
Если во входящем (REST) запросе в Header был указан атрибут authorization, то парсим и валидируем его в функции CheckGetJWTToken, которая либо возвращает ошибку, либо в случае успеха возвращает UserId и UserRole.
Далее формируем TraceId и заворачиваем его вместе с UserId и UserRole в контекст вызова и осуществляем gRPC вызов нашего микросервиса.
И, наконец, пишем в лог событие вызова сервиса.
Еще один middleware обработчик «вешаем» на ответы конкретных методов (SignIn, SignUp) сервиса User. Этот обработчик перехватывает gRPC ответы, забирает ответ UserID и UserRole, преобразует в JWT Token и отдает его (JWT Token) в REST ответе в качестве Header атрибута «authorization». Описанный middleware код реализован на стороне gRPC клиента в файле ./api-gw/services/user/protobuf/functions.go.
Подключаем обработчик ответов.
Пример — обработчик ответа SignIn (обработчик SignUp аналогичен).
Продолжение следует…
Да, демо проекта можно посмотреть здесь, а исходный код здесь.
Теперь пришла очередь API Gateway или API GW.
В нашем c ptimofeev API GW мы реализуем следующие функции:
- Конвертация REST запросов в gRPC запросы и наоборот.
- Логирование запросов.
- Аутентификация запросов.
- Присвоение каждому запросу Trace ID для дальнейшей передачи его между микросервисами по всей цепочке выполнения запроса.
Итак, поехали…
Для реализации функции конвертация REST/gRPC будем использовать гошную библиотеку grpc-gateway.
Далее в протофайле каждого микросервиса, который хотим опубликовать по REST, необходимо добавить описание option в секции описания интерфейсов сервиса. Здесь собственно указывается путь и метод, по которому будет осуществляться доступ по REST.
//Описание сервиса Category service CategoryService { //Создание записи rpc Create (CreateCategoryRequest) returns (CreateCategoryResponse) { option (google.api.http) = { post: "/api/v1/category" }; } }
На основе этой информации скрипт генерации кода (./bin/protogen.sh) создаст код gRPC сервера (в каталоге микросервиса), gRPC клиента (в каталоге api-gw) и сгенерирует актуальную документацию API (в формате {{имя сервиса}}.swagger.json)
Далее нам нужно написать код HTTP Proxy, который с одной стороны будет HTTP сервером (для обработки REST запросов), а с другой стороны будет gRPC клиентом для наших микросервис��в (gRPC серверов).
Этот код мы разместим в файле ./services/api-gw/main.go.
В начале в секции import подключаем клиентские библиотеки к наших микросервисов
(их нам сгенерил protogen.sh):
import ( … userService "./services/user/protobuf" postService "./services/post/protobuf" commentService "./services/comment/protobuf" categoryService "./services/category/protobuf" …
Далее указываем адреса и порты на которых «висят» наши gRPC сервисы (значения берем из переменных окружений):
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")) )
И, наконец, реализуем сам HTTP Proxy:
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)) }
В настройке подключения к микросервисам мы используем опцию grpc.WithUnaryInterceptor(AccessLogInterceptor), в которую в качестве параметра передаем функцию AccessLogInterceptor. Это не что иное как реализация middleware слоя, т.е. функция AccessLogInterceptor будет выполняться при каждом gRPC вызове дочернего микросервиса.
… //---------------------------------------------------------------- //Подключение к сервису Post grpcPostConn, err:=grpc.Dial( … grpc.WithUnaryInterceptor(AccessLogInterceptor), … )
В свою очередь, в функции AccessLogInterceptor мы уже реализуем механизмы аутентификации, логирования и генерации TraceId.
Если во входящем (REST) запросе в Header был указан атрибут authorization, то парсим и валидируем его в функции CheckGetJWTToken, которая либо возвращает ошибку, либо в случае успеха возвращает UserId и 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"]) } }
Далее формируем TraceId и заворачиваем его вместе с UserId и UserRole в контекст вызова и осуществляем gRPC вызов нашего микросервиса.
//Присваиваю 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...)
И, наконец, пишем в лог событие вызова сервиса.
msg:=fmt.Sprintf("Call:%v, traceId: %v, userId: %v, userRole: %v, time: %v", method,traceId,userId,userRole,time.Since(start)) app.AccesLog(msg)
Еще один middleware обработчик «вешаем» на ответы конкретных методов (SignIn, SignUp) сервиса User. Этот обработчик перехватывает gRPC ответы, забирает ответ UserID и UserRole, преобразует в JWT Token и отдает его (JWT Token) в REST ответе в качестве Header атрибута «authorization». Описанный middleware код реализован на стороне gRPC клиента в файле ./api-gw/services/user/protobuf/functions.go.
Подключаем обработчик ответов.
func init() { //Переопределяю обработку ответа для вызовова SignIn forward_UserService_SignIn_0 = forwardSignIn //Переопределяю обработку ответа для вызовова SignUp forward_UserService_SignUp_0 = forwardSignUp }
Пример — обработчик ответа SignIn (обработчик SignUp аналогичен).
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...) }
Продолжение следует…
Да, демо проекта можно посмотреть здесь, а исходный код здесь.
