How to Install Kong Custom Lua and Go Plugin

There is no doubt that Kong is one of the best API gateways on the market. One of its cool features is that you can extend its functionality by writing custom plugins. I’ve got a couple viewers asking me to talk about how to install custom plugins, that’s what I am going to talk about in this post.

Kong used to support Lua plugins only but you can use Go, Javascript or Python plugins as of version 2.4. Because Javascript PDK and Python PDKs are quite new, I will only cover the method of installing Lua and Go plugins in this post.

Please note this post is about INSTALLING plugins. If you are looking for a tutorial about developing kong plugins, please read official post.

Prerequisites:
Docker: You need to have Docker installed and understand the basics of Docker.
Terminal: We will use terminal to run docker container and build docker images.
JQ: We will use JQ to filter outputs.

Start Kong

I will run Kong in DBless mode for our demo, container name is kong-demo. To start this Kong container, let’s run below command.

Please note the container will be removed when it stop. If you don’t want this behaviour, please remove --rm flag from the command. I am also running this container in detached mode as it is easier to play with.

1
2
3
4
5
6
docker run --rm --detach --name kong-demo \
-p "8000-8001:8000-8001" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
-e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
-e "KONG_DATABASE=off" \
kong:2.4-alpine

Now that we have our container running, we can install plugins.

Lua Plugin

I will be using kong-jwt2header as our example.

Install with lua files

This repo is extremely easy to use as the repo owner provides a script to install.

Clone repo

1
git clone https://github.com/yesinteractive/kong-jwt2header.git

Modify Script

Kong used to be running as root user but it is no longer the case. Therefore we need to modify this script to use Kong for reload.

Let’s cd kong-jwt2header to the folder and then use the editor of your choice to modify update_docker.sh file.

You need to change below line

1
docker exec --user root -e KONG_PLUGINS="bundled,$dir" $container kong reload - vv

to

1
docker exec --user kong -e KONG_PLUGINS="bundled,$dir" $container kong reload - vv

Run script

Now we can run our script.

1
./update_docker.sh

We will be asked to enter our container name. We enter kong-demo and enter.

Check plugin

1
curl http://localhost:8001 -s | jq .plugins | grep kong-jwt2header

We should see "kong-jwt2header": true in the result.

What this script does was

  1. Create folder /usr/local/share/lua/5.1/kong/plugins/kong-jwt2header inside kong-demo container.
  2. Copy two plugins files into this folder.
  3. Add environment variable KONG_PLUGINS="bundled,kong-jwt2header" and then reload Kong.

Install with Luarocks

We can also install plugins with luarocks. Luarocks is the package manager for Lua modules. As Kong has luarocks installed by default, we can easily install rock files inside container. We will still use kong-jwt2header because it is also available on luarocks website.

Install Rock file

Let’s stop and start a new kong-demo container first.

Then we run below command.

1
docker exec -it --user root kong-demo luarocks install kong-jwt2header

As you can see I am running luarocks install as root user inside the container and luarocks will fetch and install plugin automatically.

Enable Plugin and reload

We need to add environment variable KONG_PLUGINS="bundled,kong-jwt2header" and reload Kong.

1
docker exec --user kong -e KONG_PLUGINS="bundled,kong-jwt2header" kong-demo kong reload -vv

Check plugin

1
curl http://localhost:8001 -s | jq .plugins | grep kong-jwt2header

We should see "kong-jwt2header": true in the result as well.

Build custom image

In my opinion the best way to use custom plugin is to build a custom image because you don’t need to install plugin manually in case you need to rotate your pod.

To build a custom image is quite simple, here is an example.

  1. Please save below to Dockerfile.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    FROM alpine:latest as builder
    RUN apk add --no-cache git
    RUN mkdir /jwt2header
    RUN git clone https://github.com/yesinteractive/kong-jwt2header.git /jwt2header

    FROM kong:2.4-alpine
    USER root
    RUN mkdir /usr/local/share/lua/5.1/kong/plugins/kong-jwt2header
    COPY --from=builder /jwt2header/plugin/. /usr/local/share/lua/5.1/kong/plugins/kong-jwt2header
    USER kong
  2. Build image
    Below command builds the image and tag it as kong-demo. It will also replace existing kong-demo image if you already have one.

    1
    docker build --no-cache -t kong-demo . 
  3. Start Kong with custom image

    1
    2
    3
    4
    5
    6
    7
    docker run --rm --detach --name kong-demo \
    -p "8000-8001:8000-8001" \
    -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
    -e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
    -e "KONG_DATABASE=off" \
    -e "KONG_PLUGINS=bundled,kong-jwt2header" \
    kong-demo

Use custom plugin

As the plugin is installed and enabled, let me show you how to use this plugin.

kong-jwt2header plugin can be used to extract claim:value from a passed in JWT token and apply its value to header. It can be used to change the routing or simply add extra headers to upstream.

As we are running Kong in dbless mode, we need to pass our declarative config to Kong. You can choose to mount this file inside container so Kong will start with all the configs. However, I will post this config to kong via admin api /config endpoint.

Create Config file

Let’s save this file as test.yaml in our current folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_format_version: "2.1"
_transform: true

services:
- name: first-demo
url: https://httpbin.org/anything
routes:
- name: first-demo-route
paths:
- /demo
- name: second-demo
url: https://mockbin.org/request
routes:
- name: second-demo-route
headers:
x-kong-jwt-claim-demo:
- test
paths:
- /demo

plugins:
- config:
strip_claims: "false"
token_required: "false"
enabled: true
name: kong-jwt2header

As you can see I’ve got two services with two routes. The first route match all request go to <proxy_node>/demo and the second route match when the requests go to path <proxy_node>/demo with a header x-kong-jwt-claim-demo: test.

We also enable this plugin globally and set config.strip_claims and config.token_required as false.

Let’s apply this config with below command.

1
curl -X POST http://localhost:8001/config -F config=@test.yaml

Test Plugin

  1. Test first-demo-route
    We should reach httpbin with below request.

    1
    curl http://localhost:8000/demo
  2. Test second-demo-route
    We should reach mockbin with below request.

    1
    curl http://localhost:8000/demo --header "x-kong-jwt-claim-demo:test"
  3. Generate JWT
    You can use jwt.io to create your token. I am using JWT-cli.

    1
    jwt encode --secret=demo '{"demo":"test"}'

    We should get a JWT token like this one.

    1
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZW1vIjoidGVzdCIsImlhdCI6MTYyNDAxMDU0MH0.HxVkGI1N0woJPtjiJRYZI-xwbMJ77w9dC1QhYcoFCbU
  4. Call route with token

    1
    2
    curl http://localhost:8000/demo \
    --header "authorization:bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZW1vIjoidGVzdCIsImlhdCI6MTYyNDAxMDU0MH0.HxVkGI1N0woJPtjiJRYZI-xwbMJ77w9dC1QhYcoFCbU"

We can see we are being routed to second-demo-route because plugin extract claim demo:test from JWT token and created x-kong-jwt-claim-demo:test header, hence the new request match the second route.

Go Plugin

Go plugin runs on a process that is separated from Kong gateway itself. Therefore we need a plugin server to run go plugins. There are two method of using plugin server. The first method is to compile a standalone plugin-server. The other method is to embedded plugin server to plugin itself, please check official doc for more information.

You may be able to build plugin server executables yourself and mount it to docker container but I could not get this to work. I have to build custom image if I want to use go plugins.

I will be using Kong’s go plugin repo to show you how to build your image and use the plugin.

Below is the command I will be using . You need to substitute <ENV_VARIABLES> with the environment variables in below examples.

1
2
3
4
5
6
7
8
docker run --rm -d --name kong-demo \
-p "8000-8001:8000-8001" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
-e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
-e "KONG_DATABASE=off" \
-e "KONG_PLUGINS=bundled,go-hello" \
<ENV_VARIABLES>
kong-demo

Standalone plugin server

Build image

We will use below Dockerfile to build our image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM golang:alpine as builder

RUN apk add --no-cache git gcc libc-dev curl
RUN mkdir /go-plugins
RUN curl https://raw.githubusercontent.com/Kong/go-plugins/master/go-hello-lm.go -o /go-plugins/go-hello.go

RUN cd /go-plugins && \
go mod init kong-go-plugin && \
go get github.com/Kong/go-pdk && \
go get github.com/Kong/go-pluginserver && \
go build github.com/Kong/go-pluginserver && \
go build -buildmode plugin go-hello.go

FROM kong:2.4-alpine
COPY --from=builder /go-plugins/go-pluginserver /usr/local/bin/
RUN mkdir /tmp/go-plugins
COPY --from=builder /go-plugins/go-hello.so /tmp/go-plugins

Please save it to a Dockerfile and run docker build --no-cache -t kong-demo . This command builds the image and tag it as kong-demo. It will also replace existing kong-demo image if you already have one.

Start Kong

We just need to use below environment variables to substitute <ENV_VARIABLES> above to start Kong.

  • Old style
    Version 0.5.0 of go-pluginserver requires the old style configuration.

    1
    2
    -e "KONG_GO_PLUGINSERVER_EXE=/usr/local/bin/go-pluginserver" \
    -e "KONG_GO_PLUGINS_DIR=/tmp/go-plugins" \
  • New style
    The new style configuration requires v0.6.0 of go-pluginserver.

    1
    2
    3
    4
    -e "KONG_PLUGINSERVER_NAMES=go" \
    -e "KONG_PLUGINSERVER_GO_SOCKET=/usr/local/kong/go_pluginserver.sock" \
    -e "KONG_PLUGINSERVER_GO_START_CMD=/usr/local/bin/go-pluginserver -kong-prefix /usr/local/kong/ -plugins-directory /tmp/go-plugins" \
    -e "KONG_PLUGINSERVER_GO_QUERY_CMD=/usr/local/bin/go-pluginserver -dump-all-plugins -plugins-directory /tmp/go-plugins" \

Check plugin

1
curl http://localhost:8001 -s | jq .plugins | grep go-hello

We should see "go-hello": true in the result.

Embedded plugin server

Build image

We will use below Dockerfile to build our image. As you can see from this code, we did not build plugin server because go-hello.go has it embededded.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM golang:alpine as builder

RUN apk add --no-cache git gcc libc-dev curl
RUN mkdir /go-plugins
RUN curl https://raw.githubusercontent.com/Kong/go-plugins/master/go-hello.go -o /go-plugins/go-hello.go

RUN cd /go-plugins && \
go mod init kong-go-plugin && \
go get github.com/Kong/go-pdk && \
go mod tidy && \
go build go-hello.go

FROM kong:2.4-alpine
COPY --from=builder /go-plugins/go-hello /usr/local/bin/

Let’s run docker build --no-cache -t kong-demo . to build our image.

Start Kong

We just need to use below environment variables to substitute <ENV_VARIABLES> above to start Kong.

1
2
-e "KONG_PLUGINSERVER_NAMES=go-hello" \
-e "KONG_PLUGINSERVER_GO_HELLO_QUERY_CMD=go-hello -dump" \

Check plugin

1
curl http://localhost:8001 -s | jq .plugins | grep go-hello

We should see "go-hello": true in the result.

Use custom plugin

Same as above Lua plugin example, we need to pass our declarative config to Kong.

go-hello plugin will add a x-hello-from-go header in response as x-hello-from-go: Go says <config.message> to ${host}.

Create Config file

I only need 1 route for this demo and I enabled go-hello plugin globally with a hello from go plugin as my config.message.

I am expecting to see x-hello-from-go: Go says hello from go plugin to localhost:8000 in the response.

Let’s save this file as test.yaml in our folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_format_version: "2.1"
_transform: true

services:
- name: first-demo
url: https://httpbin.org/anything
routes:
- name: first-demo-route
paths:
- /demo

plugins:
- config:
message: hello from go plugin
enabled: true
name: go-hello

Test Plugin

  1. First we apply config to Kong.

    1
    curl -X POST http://localhost:8001/config -F config=@test.yaml
  2. Test first-demo-route
    We should reach httpbin with below request.

    1
    curl http://localhost:8000/demo -i

We can see we get the expected header in the response which means the plugin is working.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl http://localhost:8000/demo -I

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 464
Connection: keep-alive
x-hello-from-go: Go says hello from go plugin to localhost:8000
Date: Mon, 21 Jun 2021 04:21:49 GMT
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Kong-Upstream-Latency: 1013
X-Kong-Proxy-Latency: 2
Via: kong/2.4.1

I understand this is a long post and it can be difficult to follow. I will make a video to demonstrate if I have time.

Thanks for reading.