gRPC-gateway as a KrakenD plugin

image from the official grpc-gateway repository

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:

Custom:

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!

Stay up to date with KrakenD releases and important updates