• Stars
    star
    213
  • Rank 185,410 (Top 4 %)
  • Language
    Clojure
  • Created over 11 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A configuration library designed to allow Clojure applications to travel painlessly between different environments.

nomad

A configuration library designed to allow Clojure applications to travel painlessly between different hosts and environments.

Usage

Set-up

Add the nomad dependency to your project.clj

[jarohen/nomad "0.9.0"]

{jarohen/nomad {:mvn/version "0.9.0"}}

Please see the Changelog for more details.

Rationale

In an ideal world, we’d choose to declare configuration as a vanilla Clojure map, and access it as such, from anywhere, and everything would Just Work™.

Nomad aims to get as close to that as possible. In doing so, it makes a few opinionated decisions:

  • Configuration is declared near the code it is configuring - i.e. not in an EDN file, not in environment variables, not in a separate infrastructure repository, not in deploy scripts, etc etc etc. This locality allows us to reason about how the code will behave in certain environments without having to audit an entire system, and aids us when we come to adding new configuration variables.

    Sure, we’ll always need to allow whatever’s bootstrapping our application to alter the behaviour in some way (Nomad relies on passing in a set of ‘switches’, for example), but let’s minimise it.

  • Configuration is declared in Clojure - this gives us the full flexibility of normal Clojure functions to build/compose our configuration as necessary. In our experience, configuration libraries often tend to try to replicate a full language trying to emulate certain behaviours - retrieving configuration from elsewhere, fallbacks/defaults, environment variable parsing and composing multiple configuration files, to name a few.

    Let’s just use Clojure core/simple Java interop over a config-specific DSL - they’re good at this.

    Is there a possibility of this freedom getting abused, and the boundary between ‘configuration’ and code getting blurred? Sure. I’m trusting you to know when your configuration goes significantly beyond ‘just a map’, though.

Migrating from 0.7.x/0.8.x betas

I had a significant re-think of what I wanted from a configuration library between 0.7.x/0.8.x and 0.9.x, based on learnings from using it in a number of non-trivial applications.

  • The original 0.7.x behaviour will be maintained for the time being, in the original nomad namespace, although will be removed in a later release.
  • The 0.8.x behaviour never made it to a stable release, and so has been removed in 0.9.0-rc1.

There is a migration guide for both of these versions in the changelog.

Getting started

First, we require [nomad.config :as n].

In the entry point to our application (or, for now, at the REPL) we need to initialise Nomad - over time, we’ll need to add to this:

(n/set-defaults! {})

We then use defconfig to declare some configuration:

(n/defconfig email-config
  {:email-behaviour :just-console-for-now})

(defn email-user! [{:keys [...] :as email}]
  (case (:email-behaviour email-config)
    :just-console-for-now (prn "Would send:" email)
    :actually-send-the-email (send-email! email)))

We can see that, once we’ve declared our configuration, we use it in the same way we’d use any other vanilla Clojure data structure. We can destructure it, compose it, pass it around, play with it/redefine it in our REPL - no problem.

If I wanted to, I could use System/getenv, System/getProperty, or any vanilla Clojure functions etc in here:

(n/defconfig email-config
  {:behaviour :actually-send-the-email
   :host (System/getenv "EMAIL_HOST")
   :port (or (some-> (System/getenv "EMAIL_PORT") Long/parseLong)
             25)})

This obviates the need for any kind of config DSL to retrieve/parse/default configuration values.

Changing configuration based on location

Your configuration will likely vary depending on whether you’re running your application in development, test/beta/staging environments, or production. Nomad accomplishes this using ‘switches’, which are set in your call to set-defaults!:

(n/set-defaults! {:switches #{:live}})

You can then vary your configuration using the n/switch macro, which behaves a lot like Clojure’s case macro:

;; in your app entry point
(n/set-defaults! {:switches #{:live}})

;; in your namespace
(n/defconfig db-config
  (merge {:port 5432}
         (n/switch
           :beta {:host "beta-db-host"
                  :username "beta-username"}
           :live {:host "live-db-host"
                  :username "live-username"}

           ;; you can also provide a default, if none of the above switches are
           ;; active
           {:host "localhost"
            :username "local-user"})))

;; at the REPL (say)
(let [{:keys [host port username]} db-config]
  ;; in here, we get the live config, because of our earlier `set-defaults!`
  ...)

You’re free to choose how to select your switches - or, you can use n/env-switches, which looks for the NOMAD_SWITCHES environment variable, or the nomad.switches JVM property, expecting a comma-separated list of switches:

;; starting the application
NOMAD_SWITCHES=live,foo java -cp ... clojure.main -m ...

;; --- in the entry point
(n/set-defaults! {:switches n/env-switches})
;; sets switches to #{:live :foo}

Secrets (shh!)

Nomad can manage your secrets for you, too. Under Nomad, these are encrypted and checked in to your application repository, with the encryption keys managed outside of your application (in whatever manner you choose).

First, generate yourself an encryption key using (n/generate-key)

(nomad.config/generate-key)
;; => "tvuGp8oGGbP+IQSzidYS+oXB3fhGZLpVLhMFljL0I/o="

We then pass this to Nomad as part of the call to set-defaults!:

(n/set-defaults! {:secret-keys {:my-dev-key "tvuGp8oGGbP+IQSzidYS+oXB3fhGZLpVLhMFljL0I/o="}})

Obviously, normally, this would not be checked into your application repository! You can get it from an environment variable, an out-of-band file on the local disk, some external infrastructure management, some cloud key manager, or something else entirely - take your pick!

We then encrypt credentials using n/encrypt, and store this cipher-text, along with the key-id used to encrypt the credentials, in our defconfig declarations:

;; --- at your REPL

(n/encrypt :my-dev-key "super-secure-password123")
;; => "y/DwItK86ZgtUUTzz+sDCNd3rpsOuiyKmqcHIelHnRdrpr06k43NEnrraWrfUHE39ZXtLItqxZVM3hmCj1pqLw=="

;; --- in your namespace
(defconfig db-config
  {:host "db-host"
   :username "db-username"
   :password (n/decrypt :my-dev-key "y/DwItK86ZgtUUTzz+sDCNd3rpsOuiyKmqcHIelHnRdrpr06k43NEnrraWrfUHE39ZXtLItqxZVM3hmCj1pqLw==")})

;; access the password like any other map key
(let [{:keys [host username password]} db-config]
  ...)

Testing your configuration

Given configuration declarations are just normal Clojure variables, you can experiment with them at the REPL, as you would any other Clojure data structure.

Nomad does offer a couple of other tools to facilitate testing, though. First, defconfig declarations can be dynamically re-bound, using Clojure’s standard binding macro:

(n/defconfig email-config
  {:email-behaviour :just-console-for-now})

(defn email-user! [{:keys [...] :as email}]
  (case (:email-behaviour email-config)
    :just-console-for-now (prn "Would send:" email)
    :actually-send-the-email (send-email! email)))

(email-user! {...})
;; prints the email to the console

(binding [email-config {:email-behaviour :actually-send-the-email}]
  (email-user! {...}))
;; actually sends the email

Nomad also offers a with-config-override macro, which allows you to override what switches are active, throughout your system, for the duration of the expression body:

(n/defconfig email-config
  {:email-behaviour (n/switch
                      :live :actually-send-the-email
                      :just-console-for-now)})

(defn email-user! [{:keys [...] :as email}]
  (case (:email-behaviour email-config)
    :just-console-for-now (prn "Would send:" email)
    :actually-send-the-email (send-email! email)))

(email-user! {...})
;; prints the email to the console

(n/with-config-override {:switches #{:live}}
  (email-user! {...}))
;; actually sends the email

Bugs/features/suggestions/questions?

Please feel free to submit bug reports/patches etc through the GitHub repository in the usual way!

Thanks!

Changes

The Nomad changelog has moved to CHANGES.org.

License

Copyright © 2013-2018 James Henderson

Distributed under the Eclipse Public License, the same as Clojure.

More Repositories

1

chime

A really lightweight Clojure scheduler
Clojure
537
star
2

chord

A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.
Clojure
438
star
3

yoyo

Yo-yo is a protocol-less, function composition-based alternative to Component
Clojure
87
star
4

phoenix

A plugin for configuring, co-ordinating and reloading Components
Clojure
69
star
5

simple-brepl

A really simple plugin to start CLJS browser REPLs, built atop 'Weasel'
Clojure
44
star
6

flow

Lightweight library to help you write dynamic CLJS webapps
Clojure
38
star
7

frodo

A lein plugin to start a Ring server via configuration in Nomad
Clojure
38
star
8

clidget

Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.
Clojure
24
star
9

graph-zip

A zipper library for Clojure that navigates graph structures
Clojure
19
star
10

bounce

Bounce is a protocol-less, function composition-based alternative to Component
Clojure
17
star
11

oak

A ClojureScript library to structure single-page apps - taking inspiration from the Elm Architecture
Clojure
13
star
12

with-open

A one-macro library to extend the behaviour of Clojure's 'with-open' without having to implement Closeable.
Clojure
12
star
13

splat

A Leiningen template to create ClojureScript single page web applications.
Clojure
12
star
14

selectable

An example CLJS app replicating David Nolan's 'CSP is Responsive Design' blog, with a slightly different approach.
Clojure
11
star
15

cljs-tetris

A Tetris clone written in ClojureScript + core.async
Clojure
8
star
16

embed-nrepl

A micro-library to start up an nREPL server with my opinionated defaults
Clojure
6
star
17

advent-of-code

Clojure
6
star
18

clojurex2013

My slides and references from 'Putting the Blocks Together' at ClojureX 2013
CSS
3
star
19

neo-zip

A Clojure library to query Neo4J databases using Graph-Zip syntax
Clojure
3
star
20

wiring

A Clojure library to configure and wire-up component-based applications
Clojure
2
star
21

multi-chat

A very basic multi-player chat webapp written using ClojureScript, WebSockets and Chord
Clojure
2
star
22

dot-emacs

My Emacs dotfiles
Emacs Lisp
1
star
23

eclj

A Clojure library to execute Clojure forms embedded inside text files - similar to ERB.
Clojure
1
star
24

ghost-middleware

Some Ring middleware to handle Google's AJAX shebang spec
Clojure
1
star
25

adventures

Clojure
1
star
26

datomic-arch-package

An attempt to package up Datomic on Arch Linux
Shell
1
star