Sofeware Configuration Management

Notice: It’s irresponsible thought below, which has not yet confirmed by production environment.

A well known organized codebase should obey a rule requires strict separation of config from code as described in The Twelve-Factor App, which recommends storing config in environ variables.

There are two common ways widely used in projects:

  • Store configs in environ variables
    • Pros: Well Orthogonal
    • Cons: Without version management, namespace, inheritance
  • Batch configs into named groups
    • Pros: Version management, namespace, inheritance
    • Cons: Combinatorial explosion of configs

The first is commonly used in Heroku, the other is arguably more common in Rails, Flask, Ring apps.

Tasty wine: ENV + Groups

From the perspective of a developer, I prefer grouping config. When doing deploys, especially distinguish staging and production, I prefer ENV-way.

My preferences vacillate depending on the role I’m playing. Let’s implement a combination of those two ways to versioned config, to put configs into different namepaces, to inherit from others, to store them into ENV!

Separate config and code

We write our app in favor of ENV as what we do before. All configs were read from ENV.

def get-env(key)
  ENV[key]
end

We still need to maintain a standalone config repo to generate config, which will be written into ENV or sent to Heroku via HTTP API. That’s a big step. For now, configs were versioned.

May some steps of continuous integration be:

  • Download config repo;
  • Run config repo main function. Write configs to ENV;
  • Download code;
  • Run code.

Namespace

The environ variables may be considered a flat map without namespace. We can add prefix to config key as namespace.

For example:

def get-env(ns, key)
  ENV[transform_rule(ns, key)]
end

get-env('vanilla', 'database') # VANILLA__DOT__DATABASE

A better solution is to supply a func like clojure.core/munge.

Multiple versions

Namespace can be also used in version control.

def get-env(ns, key)
  ENV[transform_rule(ENV['VERSION'], transform_rule(ns, key))]
end

get-env('vanilla', 'database') # ONLINE__DASH__7D74032__DOT__VANILLA__DOT__DATABASE

Imagine Bob is ready to deploy code. Master’s version is ahead of production’s. He deploy config first to Heroku before deploying code. There are at least two copies of config items online now. Old one is using by production code. New one will be used by deploying code. When new code rolls back to old code, let it use old config.

DRY

Duplicate are easily created in config files like yaml:

- mc
  - localhost:11211
  - localhost:11212
  - localhost:11213
  - localhost:11214
  - localhost:11215
- db
  - localhost:7878
  - localhost:7979

But we know DRY! Process give us ability to abstract things:

localhost = lambda { |port| "localhost:#{port}" }

mc = (11211..11215).map &localhost
db = [7878, 7979].map &localhost

In a config repo, we can do better. We can even write unittest to proving correctness of configs.

Docker

The appearance of Docker eliminate the difference between production environment and development environment, which shared the same config. Things are going to be easy now. :)