How to work with Golang plugins and KrakenD

The release of golang 1.8 more than a year ago opened the door to loading dynamic linked components in run time, and we were keen to find out if we could include this great feature in our KrakenD toolbox.

We’d like to share our experience and details on how we enhanced our products to support golang plugins.

Why would anyone use plugins in Go?

The plugin concept is widely known and supported in several programming languages and environments. It gives third-party developers the ability to extend applications, adding new features or customizing behaviors, without touching a single line of the core application, while avoiding recompilation.

Our distribution of KrakenD (aka, KrakenD-CE) today includes a series of extra middleware living in separate repositories that are compiled and included in the final binary of KrakenD-CE.

As owners of the product, we have to decide what kind of middleware to include by default in the final distribution, but we understand that our reasoned decision not to include something might not fit your particular interests. Our compromise is to have a pure API Gateway with the best possible performance, and some features will always drop out by their nature and will never join the krakend-ce. This forces users to import this middleware and compile the final binary themselves.

Here’s a practical example: if you wanted to include the New Relic instrumentation middleware to see metrics in your panel, then you would have to fork the KrakenD-CE repository, import the package, add the middleware in the pipe factory and compile the project. While that’s no big deal, it would be much easier to just put a file in a folder and start the server, wouldn’t it?

If you’re wondering why the NR module is not included in our distribution (yet?) it’s because adding the agent into the gateway has a high impact in terms of performance and memory consumption, so until the maintainer fixes this issue, it’s out.

Good news and bad news

The good news is that with plugins we can give a lot of power to KrakenD without jumping into a discussion about whether this specific set of features should go inside the distribution or not (or at least, a less heated discussion).

The bad news is that as the official documentation says “Plugin support is currently only available on Linux”. Although 99.9% of users deploy in production in a Linux box or a linux container, right now KrakenD is multiplatform but plugins are not.

Tuning the KrakenD framework

Let’s get our hands dirty and take look at an example of the Go code you need to support plugins.

For the initial iteration, we just added a new config section and a single plugin package.

Here is the definition of the new config struct added in the ServiceConfig:

type Plugin struct {
    Folder  string `mapstructure:"folder"`
    Pattern string `mapstructure:"pattern"`
}

In the configuration file we can now define the location of the folder with the plugins and a pattern for filtering the contents of the folder. A typical configuration snippet could go like this:

"plugin": {
    "folder": "./plugins/",
    "pattern": ".so"
}

The entire package in charge of scanning the plugin folder and loading all the included plugins exposes a single function func Load(cfg config.Plugin) (int, error), so it is convenient to consume.

func open(pluginName string) (err error) {
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            err, ok = r.(error)
            if !ok {
                err = fmt.Errorf("%v", r)
            }
        }
    }()
    _, err = pluginOpener(pluginName)
    return
}

// pluginOpener keeps the plugin open function in a var for easy testing
var pluginOpener = plugin.Open

We decided that delegating the registering logic to the plugins would give us a better decoupling (the good old IoC principle). After scaning and filtering the contents of the plugins folder, the Load function just calls plugin.Open wrapping the possible errors and panics. Currently it doesn’t use the returned *plugin.Plugin for lookups. Check the documentation for more details.

This is the Pull Request with all the required changes for the framework.

Tuning the KrakenD-CE distribution

In order to load the plugins before starting the gateway, the executor.go script needs to be extended with this code block:

...
    if "" != os.Getenv("KRAKEND_ENABLE_PLUGINS") && cfg.Plugin != nil {
        logger.Info("Plugin experiment enabled!")
        pluginsLoaded, err := plugin.Load(*cfg.Plugin)
        if err != nil {
            logger.Error(err.Error())
        }
        logger.Info("Total plugins loaded:", pluginsLoaded)
    }
...

Because the plugin feature is still experimental, it should be enabled both in the configuration file and in the KRAKEND_ENABLE_PLUGINS env_var.

This is the complete Pull Request.

Writing plugins

As required by the plugin Golang package, plugins should have a main package so their main function won’t ever be called.

The Elastic Search martian plugin is a very clear example of how to bundle the plugins and the KrakenD-CE binary via the krakend-martian package and without using the martian lib in the plugin in order to avoid flag collisions.

This is all the code required for the plugin:

package main

import (
    "github.com/devopsfaith/krakend-martian/register"
    "github.com/kpacha/martian-components/body/elastic-search/modifier"
)

func init() {
    register.Set("body.ESQuery", []register.Scope{register.ScopeRequest}, func(b []byte) (interface{}, error) {
        return modifier.FromJSON(b)
    })
}

func main() {

}

The github.com/devopsfaith/krakend-martian/register package exposes a setter and a getter which delegate registering of the martian modules to the plugins themselves, so the martian lib is included just once in the github.com/devopsfaith/krakend-martian package. Every plugin should register its modifiers in their init functions.

Compile the desired package with the plugin flag:

$ go build -buildmode=plugin -o krakend-martian_es.so ./krakend-plugin/elastic-search

And place the generated plugins into your plugin folder, so the KrakenD can load them in runtime.

Word of advice

These are the caveats we’ve found so far in our experiments, but almost all cases can be avoided with some workarounds:

Dependency versions

Shared libraries between the application and the plugins should have the same version or the plugin won’t be loaded. Using vendors might help limiting the friction and avoid problems but in some cases this is just not possible.

Flag collisions

If the plugin or its dependencies redeclare a flag used by the application (or its dependencies) the plugin will panic.

Vendored packages

If your application or your plugin use any kind of dependency vendoring system, the vendored packages will be usually renamed to path/to/your_app/vendor/path/to/dependency. So it won’t be accessible from your plugins.

Conclusion

In this post we have seen the benefits of using plugins and how to use them in your application. Even if they are only supported in Linux, it’s a very convenient way to extend functionality, especially those that aren’t used in all cases and are more rare.

Thanks for reading! If you like our product don’t forget to star our project!