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

Krakend Flow diagram
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

custom krakend plugins diagram

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. http://localhost:8001/v1/users/
  2. http://localhost:8001/v1/posts/

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

Categorized in:

Tagged in:

,