In this tutorial, we will learn how to make a Krakend plugin for a custom rate limiter using Golang and Docker. KrakenD is an open-source API Gateway for microservices. It is one of the fastest API gateways available in the market which is stateless and built for a distributed architecture. The following diagram illustrates the KrakenD flow
It is straightforward to set up and run a KrakenD server or deploy it on a Docker container. I will focus primarily on Docker containers and if you want to learn more about installation, please visit this link.
Download the source code from Github, if you want to check the complete code of the tutorial
Using KrakenD docker
Let’s try our hands at running a KrakenD Docker image now. Run the following command
docker run -p 8001:8001 -v $PWD:/etc/krakend/ devopsfaith/krakend run --config /etc/krakend/krakend.json
This command exposes our APIs listed in krakend.json configuration file to port 8001. If you want to generate a configuration tool online, you may use this link. So, let’s explore the contents of our krakend.json configuration file.
{
"$schema": "https://www.krakend.io/schema/v3.json",
"version": 3,
"name": "DemoKrakend",
"timeout": "3000ms",
"cache_ttl": "300s",
"endpoints": [
{
"endpoint": "/v1/users/",
"method": "GET",
"backend": [
{
"url_pattern": "/public/v2/users",
"encoding": "json",
"sd": "static",
"method": "GET",
"disable_host_sanitize": false,
"host": ["https://gorest.co.in"]
}
],
"extra_config": {
"qos/ratelimit/router": {
"max_rate": 5000
}
}
},
{
"endpoint": "/v1/posts/",
"method": "GET",
"backend": [
{
"url_pattern": "/public/v2/posts",
"encoding": "json",
"sd": "static",
"method": "GET",
"disable_host_sanitize": false,
"host": ["https://gorest.co.in"],
"extra_config": {
"qos/ratelimit/proxy": {
"max_rate": 100,
"capacity": 1
}
}
}
]
}
],
"output_encoding": "no-op",
"port": 8001
}
Let’s quickly glance through the above config file. We have two API endpoints defined, namely /v1/users
and /v1/posts
. They call https://gorest.co.in/public/v2/users and https://gorest.co.in/public/v2/posts in the backend respectively.
We have defined extra_configs
in both of these endpoints but for /v1/users, the config is called in the router layer, and for the /v1/posts, it is called in the proxy layer. We will discuss the layers in detail in the next section. The important point to note is that we have done a basic rate limiting using the default mechanism shipped along with KrakenD.
Plugins and Middlewares
KrakenD plugins are binaries compiled using custom code that run parallel to the KrakenD application. They can participate in processing a request or response and can be written in Lua or Golang. Golang is better suited if you need power, speed, and control.
Middlewares, on the other hand, are custom code written on top of KrakenD source code and recompiling the binary. However, Middlewares can be the fastest but writing them is not recommended as it is prone to breakdown during version upgrades.
Type of Plugins
There are four different types of plugins we can write with KrakenD. Each of the plugins sits on a different level of the gateway and has different capabilities and power. Let’s explore them.
- HTTP server plugins (or handler plugins): They sit on the router layer and let you do anything as soon as the request hits KrakenD. As an example, you can modify the request before KrakenD starts processing it, block traffic, make validations, change the final response, connect to third-party services, databases, or anything else you imagine. You can have multiple plugins stacked together.
- HTTP client plugins (or proxy client plugins): They sit on the proxy layer and let you change how KrakenD interacts (as a client) with a specific backend service. They are very powerful as server plugins, but their working influence is smaller. You can have one plugin for the backend call.
- Response Modifier plugins: They are only modifiers and let you change the responses received from your backends. These are lighter than the plugins described above.
- Request Modifier plugins: As with the response modifiers, they let you change the requests sent to your backends.
Custom KrakenD Plugin Code
In this tutorial, we will build an Http Server plugin to custom rate limit the requests in Golang. KrakenD provides a template for creating a plugin which can be found here. We will use this template to build upon our custom plugin.
The idea behind our rate limiter plugin is to consume tokens with each request. Once the tokens are exhausted, the backed APIs won’t be allowed to access further until there are new tokens in the bucket stock. The stock is updated every minute and has a maximum bucket capacity. Let’s define a few custom variables for our code
var BucketCapacity int = 10
var BucketStock int = 10
var TokenRate int = 2
BucketCapacity is the maximum number of tokens our bucket can hold. BucketStock is the current number of tokens in the bucket. After BucketStock becomes 0, backend APIs are not allowed to access. This means APIs can be called only a fixed number of times per minute. TokenRate is the number of new tokens to be added to the BucketStock each minute
Our custom KrakenD plugin will consist of four custom functions to handle different functionalities. They are explained below
Initializing a Scheduler
A scheduler will update our token bucket stock every minute till the bucket capacity is reached
// Initializes the custom plugin's background jobs
func initPlugin(){
time.Now()
for true {
time.Sleep(time.Minute)
go MinuteUpdates()
}
}
The code above contains a perpetual loop inside which a time.Sleep(time.Minute) is called. This pauses the loop and only runs the following function call every minute. The next line calls a function using a goroutine to update the bucket stock
Updating Bucket Stock
This function updates the token bucket stock
// Function for minute updates
func MinuteUpdates(){
if BucketStock < BucketCapacity{
if BucketStock + TokenRate >= BucketCapacity{
BucketStock = BucketCapacity
} else {
BucketStock += TokenRate
}
}
}
The code above fills up the token bucket stock. Every minute new tokens are added to the bucket as defined by TokenRate. If the bucket capacity is reached, the plugin doesn’t add new tokens to the bucket.
Consume Token
Each API request to our custom KrakenD plugin will consume a token from the BucketStock. Look at the following function
// Consume tokens from BucketStock
func Consume()(bool, string) {
if BucketStock > 0 {
BucketStock--
return true, ""
}
return false, "Token limit reached. Please wait for a minute"
}
Here, we are checking if the BucketStock is more than 0. If it is greater than 0, then we deduct a single token from the BucketStock, else we return an error message.
Bucket Stock Tracker
To inspect the number of available tokens in the BucketStock, we will write a small function that can be accessed via a public URL defined by trackerPath.
func Tracker() string {
return fmt.Sprintf("Total available tokens: %d/%d", BucketStock, BucketCapacity)
}
Plugin Registration with KrakenD
We will modify the existing plugin skeleton provided by KrakenD and add our code like below
func (r registerer) registerHandlers(_ context.Context, extra map[string]interface{}, h http.Handler) (http.Handler, error) {
// The config variable contains all the keys you have defined in the configuration
// if the key doesn't exists or is not a map the plugin returns an error and the default handler
config, ok := extra[pluginName].(map[string]interface{})
if !ok {
return h, errors.New("configuration not found")
}
// The plugin will look for these configurations:
trackerPath, _ := config["trackerpath"].(string)
// Initiate the plugin background jobs
go initPlugin()
// return the actual handler wrapping or your custom logic so it can be used as a replacement for the default http handler
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// If the requested path is not what we defined, continue.
if req.URL.Path == ""{
http.NotFound(w, req)
}
if req.URL.Path != trackerPath{
status, eMsg := Consume()
if !status {
http.Error(w, eMsg, http.StatusForbidden)
return
}
h.ServeHTTP(w, req)
return
}
if trackerPath != ""{
fmt.Fprintf(w, Tracker())
}
}), nil
}
The code above only shows the modified code in registerHandlers function. For the rest, we do not need any changes.
The line trackerPath, _ := config["trackerpath"].(string)
tells the plugin to look for the tracker URL. This URL will be used to check the BucketStock and BucketCapacity.
Then, go initPlugin()
initiates the one-minute scheduler function to update the tokens in the bucket as described above.
Inside the return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request){}, we have added the following
if req.URL.Path == ""{
http.NotFound(w, req)
}
if req.URL.Path != trackerPath{
status, eMsg := Consume()
if !status {
http.Error(w, eMsg, http.StatusForbidden)
return
}
h.ServeHTTP(w, req)
return
}
if trackerPath != ""{
fmt.Fprintf(w, Tracker())
}
The code above first checks if the provided URL is empty. If it is empty then throws an error. Next it checks if the provided URL is the same as the trackerPath . If so, it calls the Tracker() function, else it decrements a token from the BucketStock and allows the access to backend API URL.
This brings an end to our code part. Now we will prepare our krakend.json file for plugin injection.
Plugin Injection
This step describes how we call our plugin inside the KrakenD using the krakend.json. Our plugin is a service-level plugin, meaning, it will reside in the root of the config file. Here is the krakend.json file for this tutorial
{
"$schema": "https://www.krakend.io/schema/v3.json",
"version": 3,
"name": "krakend-ratelimiter-plugin",
"timeout": "3000ms",
"cache_ttl": "300s",
"plugin": {
"pattern": ".so",
"folder": "/etc/krakend/plugins/"
},
"output_encoding": "no-op",
"port": 8001,
"extra_config": {
"plugin/http-server": {
"name": ["krakend-ratelimiter-plugin"],
"krakend-ratelimiter-plugin": {
"trackerPath": "/__bucket-tracker"
}
}
},
"endpoints": [
{
"endpoint": "/v1/users/",
"method": "GET",
"backend": [
{
"url_pattern": "/public/v2/users",
"encoding": "json",
"sd": "static",
"method": "GET",
"disable_host_sanitize": false,
"host": ["https://gorest.co.in"]
}
]
},
{
"endpoint": "/v1/posts/",
"method": "GET",
"backend": [
{
"url_pattern": "/public/v2/posts",
"encoding": "json",
"sd": "static",
"method": "GET",
"disable_host_sanitize": false,
"host": ["https://gorest.co.in"]
}
]
}
]
}
Notice the plugin and extra_config properties in the JSON file. The plugin tells KrakenD to look for the plugin in /etc/krakend/plugins/ folder with the provided file extension. In extra_config, we pass the name of the plugin and other configuration parameters. In our case, we are just passing the tracker URL.
Other important parameters to note are the endpoints and their backends. The port number is set to 8001
Building the Custom KrakenD Plugin
For plugin development, we need to use the same version of Golang and the libraries as the ones used by KrakenD. Else the code may not run properly. To eliminate such possibilities, we can use the Docker image provided by KrakenD to generate such a build. Each version of KrakenD has a corresponding KrakenD Builder image. We are using KrakenD v2.2.1 and the builder version used is builder 2.2.1.
Step 1: Start the docker image
docker run -it -v "some_path:/go/src/krakend-ratelimiter-plugin" --name builder krakend/builder:2.2.1
Step 2: Copy the plugin code to docker
Now, assuming you are inside your project root, copy the files to the docker image
docker cp . builder:/go/src/krakend-ratelimiter-plugin
Step 3: Build the plugin
This command will generate a krakend-ratelimiter-plugin.so file inside the docker container
docker run -it -v "some_path:/go/src/krakend-ratelimiter-plugin" -w /go/src/krakend-ratelimiter-plugin krakend/builder:2.2.1 go build -buildmode=plugin -o krakend-ratelimiter-plugin.so .
Step 4: Copy the .so file to the local machine
docker cp builder:/go/src/krakend-ratelimiter-plugin/krakend-ratelimiter-plugin.so .
Running the plugin
Now that we have the plugin file and the krakend.json, we will create a docker image with KrakenD v2.2.1 and run it. Before that, we will copy our plugin and the krakend.json files inside the docker image.
Dockerfile
FROM devopsfaith/krakend:2.2.1
COPY krakend.json /etc/krakend/krakend.json
COPY krakend-ratelimiter-plugin.so /etc/krakend/plugins/krakend-ratelimiter-plugin.so
Build and run the Docker file
docker build . -t custom-krakend-plugin
docker run -it -p 8001:8001 custom-krakend-plugin
If everything worked fine, you can check
To check bucket details http://localhost:8001/__bucket-tracker
Conclusion
In this tutorial, we covered how to create a custom KrakenD plugin using Golang. Also, we learned how we can deploy the plugin using Docker. With KrakenD plugins, you can write any custom code in parallel to running the KrakenD instance without actually modifying its code. This is better as compared to writing a Middleware as it cannot be shipped with the latest versions.
If you want to read more blogs like this, please explore our homepage: https://techpro.ninja