Configuration handling in Clojure projects
One of the difficult points when managing software projects, is the separation of configuration and source code. Often when you start a project to get started quickly, you just hard-code the password of your development database in the source code until your application gets more mature and you need to have it configurable outside of your code, so that you can change the config without the need to recompile. To these purposes there are some useful Clojure libraries, cprop and mount, which help you to retrieve configuration values and to make them accessible in the source code. Both libraries are used by Luminus.
Let’s start with the cprop library. It loads configurations from diverse sources like resource files, file system files, system properties and environment variables. Let’s have a look how to use it.
First we need to include the dependency to our Leiningen project.
Next we need to require the library in a namespace where we want to collect the configuration:
Finally with the function load-config
we can load the config into a map. To use the default settings, just call the function without any parameters:
By default cprop would load the config from the classpath resource config.edn and (if provided) would then merge this config with the config file specified by the system property conf. If something is configured both in the classfile file and in the system property file, the latter wins.
With this setup, you can store your default properties in a classpath file and offer a possibility to override them in a custom file when you deploy the application.
You shouldn’t store your development settings in the classpath and rely that someone will override them in a config file as this may expose the installation, if for example the custom config file gets deleted or moved on the production server and your application uses some default that are not secure for production. Better only store useful defaults in the classpath config file and provide a separate config for your development environment.
Of course there are other possibilities apart from the default to load configurations. You can also configure a custom resource file (from classpath) or a custom file name. Furthermore you can provide pass configuration via system properties or via environment variables. Here is an example how to use all of these possibilities:
The order of the sources is relevant, the first ones are overwritten by later ones. Note that you can also use Clojure maps as list element of the merge:
There are more possibilities to sources for configurations, especially other file formats. You can have a look at the cprop docu for more info.
To use different configurations for different environments, we can inject the development config via the classpath by using different resource folder to the classpath or by setting the conf
environment variable differently depending on your environment. These two things can be managed via profiles in your project.clj.
Here an example taken from the Luminus template. I removed the not relevant parts.
In these profiles, both the source-paths and the resource-paths are extended with additional folders. To load configurations the resources-paths are more relevant, as you can add files here that will be loaded via (source/from-resource "/path/to/resource")
. The source-paths is more interesting if you want to infect different implementation of functions or objects, but could also be used if you import some maps containing configurations from there.
Via the :jvm-opts
we can override the conf
system variable and this way use specific environment specific config files.
Note that for the :dev
and :test
profiles we use multiple other profiles to fill them. It’s always the last profile of the list that overrides the previous ones, if some config parameters are specified in more than one subprofile.
As we don’t want to have to read the entire config again every time we need it, we can manage it using the mount library. With this library you can not only manage configuration objects, but application state in general, for example db connection objects could be managed stored here.
For example, in the default Luminus template, the configuration is stored in a variable called env:
You can access it later like a normal variable by importing it into the namespace you want to use it in. The start
function is only executed once and its result is bound to the state name (here env
).
You can also specified a second function with :stop
that will be executed when the application is stopped. You could for example disconnect some connections here.
The order of execution of the :start
functions of states is handled automatically by Clojure when resolving the :require references. When shutting down, the inverse order is used to execute the :stop functions.
Now you know the tools with which you can configure your Clojure projects in a clean way without infecting your production artefact with settings for the development environment.