KrakenD is a stateless, lightweight, extensible API Gateway written in Go that ships with many plugins that allow the API Gateway to adapt to differing requirements. In addition to the included plugins, KrakenD supports custom Golang plugins that load at startup via the configuration. This design decision supports unique requirements not fully covered by the standard plugins.
This article will walk you through creating a plugin for KrakenD that supports all three available plugin interfaces. Before proceeding, it’s important to note that the plugin needs to match the build arguments and module version of the target KrakenD binary.
The distribution of KrakenD used below, krakend-ce, is maintained at KrakenD, and the core framework is maintained by the Lura Project, a Linux Foundation project.
Following the recommendations from the KrakenD documentation page, use a docker container to encapsulate the build and linting steps.
Note the following commands are to support a build on an M1 Macbook. Otherwise, they should be functionally the same as the krakend.io site.
This first image is used to run the KrakenD community edition and check the plugin for compatibility.
Now, loosely following the krakend http server plugin documentation, create an empty directory with a main.go
file. The directory will be the home of a new go module. In the main.go
file, let’s go ahead and add the following lines.
package main
import (
"context"
"fmt"
"net/http"
)
func main() {}
//Names of each plugin - used for registration and configuration retrieval. It can be thought of as a unique namespace
var serverPluginName = "server-plugin"
var clientPluginName = "client-plugin"
var modifierPluginName = "modifier-plugin"
The following command will check the plugin for compatibility issues.
docker run -it --platform linux/arm64 -v "$PWD:/app" -w /app devopsfaith/krakend:2.5.0 krakend check-plugin
The following command will build the plugin using the same dependencies as the KrakenD binary.
docker run -it --platform linux/amd64 -v "$PWD:/app" -w /app krakend/builder:2.5.0 go build -buildmode=plugin -o krakend_plugins.so .
KrakenD supports three plugin types, each with varying access levels to the core components and varying support for chaining. In this example code, we will create a plugin demonstrating when the KrakenD core server would invoke each of the functionalities.
The core framework opens each plugin type similarly. KrakenD uses the plugin package from the standard library to drive this process. This package provides two critical functions,Open(filepath string)(*Plugin, error)
and Lookup(symbolName string)(Symbol, error)
, that power the KrakenD plugin experience. The Open function will load a shared object (.so) file from the provided path. KrakenD will use this to retrieve any shared objects from the filesystem using a path provided in the configuration. The system uses a well-known exported variable to expose specific plugin hooks from the shared object. The Lookup
function in the standard library retrieves the correct implementation, which is then referenced by the system.
This plugin functionality has access to everything for every request coming to the API Gateway. This plugin type is quite powerful and allows full customization of the request or response. This type provides full access to the request via the http.Handler
interface that’s passed in. The handler could rewrite URLs, modify headers, or even forward requests based on arbitrary conditions. Check out the KrakenD documentation for more information.
The RegisterHandlers function implements the Registerer interface in the KrakenD transport/http/server/plugin package. The exported HandlerRegisterer
function variable allows KrakenD to retrieve the RegisterHandlers implementation.
var HandlerRegisterer = registerer(serverPluginName)
type registerer string
func (r registerer) RegisterHandlers(registerFunc func(
name string,
handler func(context.Context, map[string]interface{}, http.Handler) (http.Handler, error),
)) {
//r.registerHandlers is a private func linked to registerer
registerFunc(string(r), r.registerHandlers)
}
The private function pattern illustrated by registerHandlers is a powerful mechanism to encapsulate the plugin logic. It’s performing two crucial tasks. First, the handler function can parse the extra map[string]interface{}
parameter to retrieve the configuration values for this plugin. It can do this by looking for the plugin name as a key in the extra
map. Example configuration parsing is omitted from the below example for brevity.
Next, it returns a closure that can reference the loaded configuration. The KrakenD gin middleware calls the returned function.
func (r registerer) registerHandlers(_ context.Context, extra map[string]interface{}, h http.Handler) (http.Handler, error) {
// Parse extra to retrieve configuration values if necessary; use those in the closure below
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fmt.Printf("[PLUGIN: %s] Request received in server plugin: %v\n", serverPluginName, req)
// call the handler
h.ServeHTTP(w, req)
}), nil
}
These plugins can be added to the endpoint to modify or alter behavior based on requests or response information. The request/response information is encapsulated in unique types and is passed in from the KrakenD core. The interface parameter implements either the RequestModifier or ReponseModifier interfaces. These types define getter functions that provide access to the underlying fields like request parameters, request/response headers, or the response status code.
The Request or Response modifier can handle endpoint-specific rate limiting, billing, or logging. As its name suggests, these plugin types can alter the request or response payloads.
The RegisterModifiers function implements the Registerer interface in the KrakenD proxy/plugin package. The exported ModifierRegisterer
function variable allows KrakenD to retrieve the RegisterModifiers implementation.
var ModifierRegisterer = registerer(modifierPluginName)
type registerer string
func (r registerer) RegisterModifiers(registerFunc func(
name string,
factoryFunc func(map[string]interface{}) func(interface{}) (interface{}, error),
appliesToRequest bool,
appliesToResponse bool,
)) {
//r.requestDump is a private func linked to registerer, omitted for brevity
registerFunc(string(r), r.requestDump, true, false)
}
The client plugin or interface allows customization of the requests to the different backends of the HTTP services. It cannot be chained with other client plugins and is unique to the backend configuration.
Similar to the Handler plugin, the RegisterClients function in the client plugin implements the Registerer interface in the KrakenD transport/http/client/plugin package. The exported ClientRegisterer
function variable allows KrakenD to retrieve the RegisterClients implementation.
var ClientRegisterer = registerer(clientPluginName)
type registerer string
func (r registerer) RegisterClients(registerFunc func(
name string,
handler func(context.Context, map[string]interface{}) (http.Handler, error),
)) {
//r.registerClients is a private func linked to registerer
registerFunc(string(r), r.registerClients)
}
Copy the shared object (*.so) file(s) to the plugin directory in your KrakenD image/volume. This directory location is configurable and needs to be set using the configuration for the service to pick up the plugins. I use a simple docker-compose definition to copy and link everything appropriately in this example.
version: "3"
services:
krakend_ce:
image: devopsfaith/krakend:2.5
platform: linux/amd64
volumes:
- ./.:/etc/krakend
ports:
- "8080:8080"
command: ["run", "-d", "-c", "/etc/krakend/krakend.json"]
{
"version": 3,
"port": 8080,
"name": "KrakenD plugin demo",
"plugin": {
"pattern": ".so",
"folder": "/etc/krakend"
},
"extra_config": {
"plugin/http-server": {
"name": [
"server-plugin"
]
}
},
"endpoints": [
{
"endpoint": "/test",
"extra_config": {
"plugin/req-resp-modifier":{
"name": [
"modifier-plugin"
]
}
},
"backend": [
{
"url_pattern": "/orgs/",
"host": "localhost:9090",
"extra_config": {
"plugin/http-client": {
"name": "client-plugin"
}
}
}
]
}
]
}
The example output shows the invocation of the different plugin types as the request passes through them. This toy example demonstrates a good starting point for developing a specific plugin type.
krakend_plugins-krakend_ce-1 | [PLUGIN: server-plugin] Request received in server plugin: &{GET /test HTTP/1.1 1 1 map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8] Accept-Encoding:[gzip, deflate] Accept-Language:[en-US,en;q=0.9] Connection:[keep-alive] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[none] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] 172.21.0.1:56540 /test <nil> <nil> <nil> 0xc000b080a0}
krakend_plugins-krakend_ce-1 | [PLUGIN: modifier-plugin] Request received in modifier plugin: &{GET <nil> map[] /test {} map[] map[User-Agent:[KrakenD Version 2.5.0] X-Forwarded-For:[172.21.0.1] X-Forwarded-Host:[localhost:8080]]}
krakend_plugins-krakend_ce-1 | [PLUGIN: client-plugin] Request received in client plugin: &{GET http://localhost:9090/orgs/ HTTP/1.1 1 1 map[User-Agent:[KrakenD Version 2.5.0] X-Forwarded-For:[172.21.0.1] X-Forwarded-Host:[localhost:8080]] {} <nil> 0 [] false localhost:9090 map[] map[] <nil> map[] <nil> <nil> <nil> 0xc0004d1050}
krakend_plugins-krakend_ce-1 | [GIN] 2024/01/11 - 05:31:35 | 200 | 4.184791ms | 172.21.0.1 | GET "/test"