gRPC-gateway as a KrakenD plugin
The gRPC protocol is becoming trendy in the era of microservices. Its compactness and backward-compatibility make it very attractive. However, it requires custom code to work with it. In this article, we’ll show you how to get all the benefits from the gRPC protocol and the gRPC-gateway without coding any business logic to use your gRPC services as regular backends. Moreover, avoiding the extra network hop!
This article begins with some introduction to gRPC services and how to build some demos using the available definitions. If you are already familiar with gRPC and created a grpc-gateway, you can skip the first sections and jump directly to Finishing the gRPC-gateway
Disclaimer: this is not a gRPC intro. If you don’t know anything about it, consider reading the project site before going any further.
gRPC backends and gateway
Service definitions
We’ll use two of the examples of the gRPC project: helloworld
and routeguide
. So we can start by adding their definitions to the protos
folder.
Since we want to consume these services also using JSON, we must create the proper definitions (or annotations), so the gRPC-gateway code can be generated. Check the documentation for more details.
helloworld.yml
type: google.api.Service
config_version: 3
http:
rules:
- selector: helloworld.Greeter.SayHello
get: /v1/helloworld/hello/{name}
routeguide.yml
type: google.api.Service
config_version: 3
http:
rules:
- selector: routeguide.RouteGuide.GetFeature
get: /v1/routeguide/features/{latitude}/{longitude}
- selector: routeguide.RouteGuide.ListFeatures
get: /v1/routeguide/features/{lo.latitude}/{lo.longitude}/{hi.latitude}/{hi.longitude}
- selector: routeguide.RouteGuide.RecordRoute
post: /v1/routeguide/route
body: "*"
These are the contents of the protos
folder:
protos
├── helloworld.proto
├── helloworld.yml
├── routeguide.proto
└── routeguide.yml
Generating the code
As described in the project documentation, we’ll need to follow some simple steps:
- generate the grpc bindings
- generate the grpc-gateway bindings
- generate the swagger definitions
We can do it with a simple bash script:
#!/bin/bash
for i in "$@"
do :
echo "generating ${i} service"
echo " - grpc bindings"
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:. \
protos/${i}.proto
echo " - grpc-gateway"
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,grpc_api_configuration=protos/${i}.yml:. \
protos/${i}.proto
# move generated sources
mkdir -p generated/${i}
mv protos/${i}.*.go generated/${i}/.
echo " - grpc-gateway 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,grpc_api_configuration=protos/${i}.yml:. \
protos/${i}.proto
done
echo "generating static files"
# move and pack the swagger definitions
mkdir -p protos/swagger
mv protos/*.swagger.json protos/swagger/.
[ ! -d statik ] || rm -rf statik
[ ! -d gateway/statik ] || rm -rf gateway/statik
go get github.com/rakyll/statik
go install github.com/rakyll/statik
statik -src=protos/swagger
mv statik gateway/.
rm -rf protos/swagger
echo "services generated"
and invoke it with sh generate.sh helloworld routeguide
The generate.sh
will also create a package with the swagger definitions, so no static files should be attached to the final binary.
Finishing the backend gRPC services
The already generated code requires a main function and the implementation of every service interface. We can use some available example implementations or go with our custom ones.
Examples:
- https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
- https://github.com/grpc/grpc-go/blob/master/examples/route_guide/server/server.go
Custom:
- https://github.com/kpacha/krakend-grpc-gateway-post/blob/master/cmd/helloworld/main.go
- https://github.com/kpacha/krakend-grpc-gateway-post/blob/master/cmd/routeguide/main.go
Finishing the gRPC-gateway
To get your grpc-gateway up and running, we’ll need two essential things:
- Create the gateway http.Handler
- Create and start the server using that handler
We are splitting this into two steps so we will be able to play with plugins in the future
Create the gateway http.Handler
Time to use the auto-generated register functions and add the swagger file server. We need to create the http.Handler
that will be injected into a custom http.Server
, so add a file called gateway/gateway.go
with the following content:
package gateway
import (
"context"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
_ "github.com/kpacha/krakend-grpc-post/gateway/statik"
"github.com/kpacha/krakend-grpc-post/generated/helloworld"
"github.com/kpacha/krakend-grpc-post/generated/routeguide"
)
func New(ctx context.Context, helloEndpoint, routeEndpoint string) (http.Handler, error) {
gw := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
if err := helloworld.RegisterGreeterHandlerFromEndpoint(ctx, gw, helloEndpoint, opts); err != nil {
return nil, err
}
if err := routeguide.RegisterRouteGuideHandlerFromEndpoint(ctx, gw, routeEndpoint, opts); err != nil {
return nil, err
}
statikFS, err := fs.New()
if err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.Handle("/swagger/", http.StripPrefix("/swagger/", http.FileServer(statikFS)))
mux.Handle("/", gw)
return mux, nil
}
Create the gateway server
The server can be created and managed from the main package itself. It needs to call the already created gateway.New
function and define the port and the shutdown strategy:
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"github.com/kpacha/krakend-grpc-post/gateway"
)
var (
helloworldEndpoint = flag.String("hello_endpoint", "localhost:50051", "endpoint of GreeterServer")
routeguideEndpoint = flag.String("route_endpoint", "localhost:50052", "endpoint of RouteGuideServer")
port = flag.Int("p", 8080, "port of the service")
)
func main() {
flag.Parse()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mux, err := gateway.New(ctx, *helloworldEndpoint, *routeguideEndpoint)
if err != nil {
log.Printf("Setting up the gateway: %s", err.Error())
return
}
srvAddr := fmt.Sprintf(":%d", *port)
s := &http.Server{
Addr: srvAddr,
Handler: mux,
}
go func() {
<-ctx.Done()
log.Printf("Shutting down the http server")
if err := s.Shutdown(context.Background()); err != nil {
log.Printf("Failed to shutdown http server: %v", err)
}
}()
log.Printf("Starting listening at %s", srvAddr)
if err := s.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("Failed to listen and serve: %v", err)
}
}
Build and start everything
Just compile the binaries and start them
go build ./cmd/grpc-gateway
go build ./cmd/helloworld
go build ./cmd/routeguide
./routeguide &
./helloworld &
./grpc-gateway &
Now we are ready to test our grpc-gateway!
$ curl -i "http://localhost:8080/v1/routeguide/features/407838350/-746143763/407838353/-74614373"
HTTP/1.1 200 OK
Content-Type: application/json
Grpc-Metadata-Content-Type: application/grpc
Date: Wed, 08 May 2019 15:49:07 GMT
Transfer-Encoding: chunked
{"result":{"name":"Patriots Path, Mendham, NJ 07945, USA","location":{"latitude":407838351,"longitude":-746143763}}}
KrakenD plugins
Using the new branch modules, it is straightforward to create customized request executors and inject them as plugins into the KrakenD-CE binary. If we want to avoid the network hop between the KrakenD gateway and the grpc-gateway, we can inject the latter into the former as a plugin with just this code:
package main
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/kpacha/krakend-grpc-post/gateway"
)
func init() {
fmt.Println("krakend-grpc-post plugin loaded!!!")
}
var GRPCRegisterer = registerer("grpc-post")
type registerer string
func (r registerer) RegisterClients(f func(
name string,
handler func(context.Context, map[string]interface{}) (http.Handler, error),
)) {
f(string(r), func(ctx context.Context, extra map[string]interface{}) (http.Handler, error) {
cfg := parse(extra)
if cfg == nil {
return nil, errors.New("wrong config")
}
if cfg.name != string(r) {
return nil, fmt.Errorf("unknown register %s", cfg.name)
}
return gateway.New(ctx, cfg.helloEndpoint, cfg.routeEndpoint)
})
}
... // aux function 'parse' omitted
Also, compile our grpc-gateway as a golang plugin
go build -buildmode=plugin -o grpc-gateway-post.so ./plugin
With the plugin already built, we can add the required configuration to our API gateway and expose a single endpoint consuming the endpoints offered by the grpc-gateway as backends without an extra network hop between the KrakenD API gateway and the gRPC-gateway
{
"version": 2,
"name": "My lovely gateway",
"port": 8000,
"cache_ttl": "3600s",
"timeout": "3s",
"plugin": {
"pattern":".so",
"folder": "./plugin/"
},
"extra_config": {
"github_com/devopsfaith/krakend-gologging": {
"level": "DEBUG",
"prefix": "[KRAKEND]",
"syslog": false,
"stdout": true
},
},
"endpoints": [
{
"endpoint": "/user/{name}/{latitude}/{longitude}",
"backend": [
{
"host": [ "http://ignore.this" ],
"url_pattern": "/v1/helloworld/hello/{name}",
"extra_config": {
"github.com/devopsfaith/krakend/transport/http/client/executor": {
"name": "grpc-post",
"endpoints": [ "localhost:50051", "localhost:50052" ]
}
}
},
{
"host": [ "http://ignore.this" ],
"url_pattern": "/v1/routeguide/features/{latitude}/{longitude}",
"group": "feature",
"extra_config": {
"github.com/devopsfaith/krakend/transport/http/client/executor": {
"name": "grpc-post",
"endpoints": [ "localhost:50051", "localhost:50052" ]
}
}
}
]
},
]
}
Our log message should be displayed at the booting stage.
$ ./krakend run -d -c krakend.json
Parsing configuration file: krakend.json
...
[KRAKEND] 2019/05/08 - 20:47:28.079 ▶ INFO Listening on port: 8000
...
krakend-grpc-post plugin loaded!!!
[KRAKEND] 2019/05/08 - 20:47:28.107 ▶ INFO plugins loaded: 1
...
Time to test our new endpoint!
$ curl -i localhost:8000/user/foo/407838351/-746143763
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Content-Type: application/json; charset=utf-8
X-Krakend: Version 0.9.0
X-Krakend-Completed: true
Date: Wed, 08 May 2019 18:48:48 GMT
Content-Length: 139
{"feature":{"location":{"latitude":407838351,"longitude":-746143763},"name":"Patriots Path, Mendham, NJ 07945, USA"},"message":"Hello foo"}
It works!
Here, the code for this post
Summary
In this article, we shared our explorations in the golang plugins area. We’ve split the custom code required for building our gRPC-gateway into separated components for later reuse of one of them. Introducing just a few lines of code, we’ve shown how to integrate any golang component exposing an http.Handler
into the KrakenD-CE as a backend.
Did you make it this far? Do you have questions or comments? Consider joining now our #krakend channel at Gophers on Slack.
Thanks for reading! If you like our product, don’t forget to star our project!
Enjoy KrakenD!
Originally published at https://www.krakend.io on June 9, 2019.