Configuring Clojure Apps
Last week Steven Deobald asked on Twitter how to do configuration in Clojure:
#lazyweb: student is asking: a good, concise article on “how to do configuration” in clojure? /cc @nilenso @duelinmarkers @jakemcc @ghoseb
— Steven Deobald (@deobald) March 16, 2017
In this post I'll tell you how we do it at Metosin. We've build a few web applications with a Clojure backend and a ClojureScript frontend. While there's no Metosin architecture carved in stone, some recurring patterns have emerged. Usually we structure the backend with either Component or Mount. The applications are deployed to virtual servers as uberjars using Ansible. To store the configuration, we use EDN files and we load them using Maailma.
We have two configuration files:
resources/config-defaults.edn
contains a default values for every configuration variable. The defaults are suitable for local development. When the application is deployed, this file is included in the deployed JAR.- For each environment (staging/production/etc), we create a file called
config-local.edn
. It contains environment-specific overrides for the defaults, including things like port numbers and database credentials. This configuration file is deployed separately from the JAR.
We load the configuration with code like this:
(require '[maailma.core :as m])
(defn get-config []
(m/build-config
(m/resource "config-defaults.edn")
(m/file "./config-local.edn")))
Maailma does a deep merge of the configuration maps, which makes overriding the defaults easy. For example, we might have a feature flag for enabling development tools in the application. We then want to disable them in the production environment. The configuration files could look like this:
;; config-defaults.edn
{:http {:port 3000
:show-dev-tools? true}}
;; config-local.edn for production
{:http {:show-dev-tools? false}}
;; What the production application sees:
{:http {:port 3000
:show-dev-tools? false}}
If needed, you can overwrite the defaults for local development by creating a
config-local.edn
file. Sometimes we also maintain an extra configuration file
for the locally-run integration tests.
Using ComponentLink to Using Component
With Component, we pass the configuration to the parts of the system when creating them. If needed, the configuration can be also made a part of the system map:
(ns backend.system
(:require [com.stuartsierra.component :as component]
[backend.component.db :as db]
[backend.component.http :as http]
[maailma.core :as m]))
(defn get-config []
(m/build-config
(m/resource "config-defaults.edn")
(m/file "./config-local.edn")))
(defn new-system []
(let [env (get-config)]
(component/map->SystemMap
{:env env
:db (db/create (:db env))
:http (http/create (:http env))})))
The configuration gets reloaded when you restart the system.
Using MountLink to Using Mount
When using Mount, we create a state to contain the configuration
(ns backend.mount.config
(:require [mount.core :as mount :refer [defstate]]
[maailma.core :as m]))
(deftstate config
:start (m/build-config
(m/resource "config-defaults.edn")
(m/file "./config-local.edn")
(mount/args)))
The other states can then use the configure by requiring it:
(ns backend.mount.http
(:require [backend.mount.config :refer [config]]
[mount.core :as mount :refer [defstate]]))
(defn start-server [port]
;; ...
)
(defstate http
:start
(let [port (get-in config [:http :port])]
(start-server port)))
To reload the configuration files, restart the config
state. I usually reload
the whole config namespace in my editor, which
makes Mount restart the state.
AlternativesLink to Alternatives
Configuration is not a one-size-fits-all affair and Maailma is not the only thing out there. For another take on EDN configuration files, take a look at the aero library. If you prefer to store the configuration in environmental variables – like Heroku recommends – see environ.