Kong custom plugin development using Go

Syafdia Okta
Level Up Coding
Published in
6 min readMar 28, 2021

--

Since version 2.0, Kong announced Go as one of the supported languages to be used on Kong API Gateway plugins development besides Lua. Kong releases the Plugin Development Kit (PDK) for Go users on their official repository. In this article, we’ll try to develop and deploy custom plugins for Kong 2.3 using Go.

We will use Docker to build Kong plugins using the official go-plugin-tool, this image use Go version 1.13 for development. Our Dockerfile:

# Stage 1 - Build plugin
# Pull image from official go-plugin-tool.
FROM kong/go-plugin-tool:2.0.4-alpine-latest AS builder
# Copy current workspace to container.
RUN mkdir -p /tmp/go-plugins/COPY . /tmp/go-plugins/
# Build the plugin.
RUN cd /tmp/go-plugins/ && \ apk add make && \ make all
# Stage 2 - Create Kong container and add custom plugin
# Pull Kong version 2.3 from official repository.
FROM kong:2.3.3-alpine
# Copy plugin from build container to Kong container
RUN mkdir /tmp/go-plugins
COPY --from=builder /tmp/go-plugins/key-checker /usr/local/bin/key-checker# Copy Kong configuration to Kong container
COPY config.yml /tmp/config.yml
# Dump plugin infoRUN /usr/local/bin/key-checker -dump

We use Docker multi stage build to build and install our plugin. First, we build the plugin, and then we copy the newly created plugin and Kong configuration to a separate Kong container. And for this plugin development, here is our project structure:

Project structure

The structure is based on Standard Go Project Layout (for more detailed information, you can refer to embedded link), all our plugins will be placed inside cmd/kong. Each plugin will be compiled as stand-alone executable binary file. And our main code base will be placed under an internal directory. We use Clean Architecture to manage our code base.

Based on Kong plugin development documentation, there are some contracts to be implemented on our code.

  1. Define a structure type to hold configuration

We should create a struct to hold for our plugin configuration, the struct with public property will be filled automatically with Kong plugin config.

Configuration struct

Based on the struct above, the APIKey property will be filled with api_key from Kong. For example, if we use Kong declarative configuration below, the APIKey value will mysecretconsumerkey.

Kong declarative configuration

2. Write a function to create instances of config struct

We should define a function to create an instance of our config struct, and the return type should be interface{} for example:

Function for creating instance

3. Add methods to config struct to handle phases

We can implement custom logic to be executed at various points of the request processing lifecycle. Based on Kong documentation, there are some phases that will be executed during request / response life cycle:

Certificate: Executed during the SSL certificate serving phase of the SSL handshake.

Rewrite: Executed for every request upon its reception from a client as a rewrite phase handler

Access: Executed for every request from a client and before it is being proxied to the upstream service.

Response: Executed after the whole response has been received from the upstream service, but before sending any part of it to the client.

Preread: Executed once for every connection

Log: Executed once for each connection after it has been closed.

4. Add a main() function that calls server.StartServer

Since we will compile our plugin as a standalone executable, we will use an embedded server from go-pdkpackage, and start the server on our main function.

Initializing embedded server

Now, let’s get our hands dirty by developing our custom plugin. We will develop a key-checker plugin. This plugin gets the secret key from the URL query parameter and validates the given key with the stored key on our config. This simple plugin is inspired by this article and the plugin will be used for our learning purpose.

We will create a struct to hold configuration and implement the Access hook to validate every request from downstream to upstream. And then we should compare the key from the query parameter with an existing key on our configuration. If the key doesn’t match with our config key, we will respond with an HTTP 401 error, and if the given key match with our config key, the request will be forwarded to the upstream.

// KeyCheckerConfig will hold all Kong configuration data// and injected use case module.type KeyCheckerConfig struct {    APIKey        string `json:"api_key"`    useCaseModule *di.UseCaseModule}
// NewKeyChecker will create an instance of KeyCheckerConfig.func NewKeyChecker() interface{} { return &KeyCheckerConfig{useCaseModule: di.GetUseCaseModule()}}func (conf *KeyCheckerConfig) Access(kong *pdk.PDK) { // Get `key` value from URL query
key, err := kong.Request.GetQueryArg("key")
if err != nil { kong.Log.Err(err.Error()) }
// Validate given `key`, compare it with our key
// on configuration. Response with 401 if given key doesn't
// match with our key on configuration
ctx := context.Background() respHeaders := make(map[string][]string) respHeaders["Content-Type"] = append(
respHeaders["Content-Type"],
"application/json"
)
err = conf.useCaseModule.ValidateKeyUseCase.Execute(
ctx,
keychecker.ValidateKeyInput{
GivenKey: key,
ValidKey: conf.APIKey,
}
)
if err != nil { switch err { case keychecker.ErrKeyEmpty, keychecker.ErrKeyNotValid: kong.Response.Exit(401, err.Error(), respHeaders) default: kong.Response.Exit(500, err.Error(), respHeaders) } return } // Get exchange token based on given key, and append it to
// `X-Exchange-Token` header.
token, err := conf.useCaseModule.GetTokenUseCase.Execute(
ctx,
key
)
if err != nil { kong.Response.Exit(500, err.Error(), respHeaders) return } kong.Response.SetHeader("X-Exchange-Token", token)}

The code above reflects our main logic should be implemented on Access hook, you can check the completed version of the code on this GitHub repo https://github.com/syafdia/go-exercise/tree/master/src/etc/demo-kong-plugin.

Now lets, run on our custom kong plugin, just clone the mentioned repo, and run build the image using docker build -t kong-demo and run container using:

docker run -ti --rm --name kong-go-plugins \
-e "KONG_DATABASE=off" \
-e "KONG_DECLARATIVE_CONFIG=/tmp/config.yml" \
-e "KONG_PLUGINS=bundled,key-checker" \
-e "KONG_PLUGINSERVER_NAMES=key-checker" \
-e "KONG_PLUGINSERVER_KEY_CHECKER_START_CMD=/usr/local/bin/key-checker" \
-e "KONG_PLUGINSERVER_KEY_CHECKER_QUERY_CMD=/usr/local/bin/key-checker -dump" \
-e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
-e "KONG_LOG_LEVEL=debug" \
-p 8000:8000 \
kong-demo

When we try to access our Kong using an invalid key, we will get HTTP 401 response and key is not valid on the body. But when we use a valid key, we will get HTTP 200 andX-Exchange-Token on response header along with response body.

401 response from custom plugin
Additional X-Exchange-Token header from custom plugin

With Kong version 2.x we could build our custom plugin using Go, and since Kong version 2.3, each plugin can be a service, compiled as a standalone executable binary.

Reference:

--

--