Making Use of the Ballerina Config API

Edit: This post is now heavily out-of-date with the newer versions of Ballerina. Please refer to the following post for up-to-date info: https://medium.com/@pubuduf/configuration-management-in-ballerina-b952cae80c49

Ballerina has been around for a few months now. It has come a long way since its launch back in February, constantly tweaking, improving and adding things based on user feedback.

Ballerina strives to be configuration-free as much as possible in order to make it hassle free for the user. But inevitably, there will be instances where you need to be able to feed in configurations dynamically, rather than hard coding it. To address this we added a config API to Ballerina. The API is two-fold: one gives an API to the Ballerina user which will allow the user to feed in configurations from various sources. The other is for the developer who needs to take user configurations into account when developing new features. In this post, I’ll be focusing on the user level API which is available in the ballerina.config package.

Config Registry

In Ballerina, all the configurations read from different sources all end up in a central config registry. The users are given read access to this registry through an API consisting of the following two functions: 
* getGlobalValue(string key)
* getInstanceValue(string instanceId, string key)

Adding configurations to the registry is only possible through runtime parameters or a config file. We’ll take a look at what global values and instance values are in the next section.

Configuration Format

A configuration in Ballerina is simply an arbitrary key/value pair, though with a slight structure to it. There are 2 types of configurations: global configs and instance configs. The global configs can be any arbitrary key/value pair, while instance configs are grouped configurations, with each group bound to a particular tag (called an instance).

The configuration keys conform to the following format: [a-zA-Z0–9._]+
And the instance IDs should conform to the following format: \[[a-zA-Z0–9._]+\]

A sample Ballerina configuration file would look like the following:

# Global configs
ballerina.host=10.100.1.200
ballerina.http.instances=http1, http2
# Instance configs
[http1]
ballerina.http.port=8080
[http2]
ballerina.http.port=8082

As it can be seen from above, the file is a slightly structured list of key/value pairs. In a config file, all the configs from the start of the file until the start of the first instance tag (i.e: a name within brackets) are considered as global configs and can be retrieved in Ballerina code using the getGlobalValue(string key) function. The keys have to be unique within global configs. If a key is repeated, only the last assigned value will be considered as the configuration value.

Any configurations between two instance tags are considered as configurations belonging to the first of these two tags. The instance configs can be retrieved using the getInstanceValue(string instanceId, string key) function. Notice that the configuration keys can be duplicated across instances. But as in the case with global configs, within an instance, the keys have to be unique. Lines starting with a “#” are comments and are ignored.

Configuration values can also access environment and system variables. For example, when parsing the configuration, the parser would look up the values for the variable parts in the following configuration, before adding it to the config registry.

http.connection=${env:HOST}:${env:HTTP_PORT}

Suppose there are 2 environment variables “HOST=10.100.1.200” and “HTTP_PORT=9092”. Then, the effective value of the above configuration would be “http.connection=10.100.1.200:9092”.

Similarly, the user can access system properties in configuration values as well. For example, “ballerina.home=${sys:ballerina.home}” would set a configuration named “ballerina.home” which would hold the path of the Ballerina installation directory.

Configuration Hierarchy

Ballerina has the following priority hierarchy when it comes to configurations: 
1) Runtime parameters (provided through CLI)
2) Environment variables, system properties (in the specified order)
3) Config file
4) Default configurations

As mentioned earlier, Ballerina strives to be as configuration free as possible. Therefore, a lot of the default behaviour comes hard coded i.e: logging configurations (which will be another topic I’ll cover later). These have the lowest priority in the config hierarchy and users can override them by providing values of their own. Next we’ll look at each of the others in the list, in the order of increasing priority.

Ballerina Configuration File

Ok so great, we have our own configuration syntax and file. How do we make use of it? Feeding it to the Ballerina runtime is as simple as naming the config file “ballerina.conf” and placing it in the source root path (the root directory of your Ballerina source files, or in other words, the directory from which you run your Ballerina programs). Or if you wish to place it in a different place, you can do so and pass the path of the config file as a runtime parameter to the Ballerina runtime as follows:

$ ballerina run program.bal -Bballerina.conf=<PATH_TO_CONFIG_FILE>

Environment Variables

Suppose you have a config file in place, but you want to override some of the configurations during runtime. For that, you can either use environment variables or runtime parameters (which we’ll look at next). To override a value, you just simply define an environment variable with the same identifier as the configuration. However, since environment variables do not allow the use of “.” in the variable name, the names defined will have to replace “.” with “_”. For example, consider you want to override the ballerina.host configuration defined in the file above. Then, you would define an environment variable named “BALLERINA_HOST”. The value for ballerina.host will be replaced by the value defined by BALLERINA_HOST during run time.

For instance configs, to identify the tag name from the configuration key, we use two underscores (“__”) to separate them. For example, suppose you want to override the port number in “http1” instance. Then, the corresponding environment variable required would be named “HTTP1__BALLERINA_HTTP_PORT”.

Ballerina Runtime Parameters

As of Ballerina v0.95.0, support for runtime parameters has been added. Users can use the “-B” flag to set runtime parameters and they take the following form: -B<key>=<value>. Runtime parameters are added to the config registry. These have the highest priority from the configuratin methods and will override any configurations set by the other 3 methods.

For consistency, the runtime parameter keys follow similar structure to the config keys in the config config file when it comes to instance configs. To set an instance config using a runtime parameter, one can use the following format: -B[instanceID].<key>=<value>. For example, the port configuration for “http1” in the above config file can be passed as a runtime parameter as follows:

$ ballerina run program.bal -B[http1].ballerina.port=8080

So What’s Next?

So in a nutshell, the Ballerina config API provides a convenient mechanism for users to configure their Ballerina programs. While this is certainly a considerable step-up from not being able to configure at all, there are still some features to be added to Ballerina to make the config API a much more effective tool. For example, one thing we’ll be looking at next is the possibility of allowing the use of function calls in annotations to make services configurable. The usage would then look like something below:

@http:configuration {
host = config:getGlobalValue(“ballerina.http.host”);
port = <int> config:getInstanceValue(“http1”, “ballerina.http.port”);
}

So, that’s about it on the Ballerina config API for the Ballerina user. Hope this will be useful if you are looking to make use of the config API.

Also, if you have ideas on how we can improve this further, do let us know! :)