Intro

In the Twelve Factor App manifesto there is a section III. dedicated to config management. In short, it advises that the configuration of the app should come “from outside”, not from inside.

Then there is this paragraph about “grouping”:

Another aspect of config management is grouping. Sometimes apps batch config into named groups (often called “environments”) named after specific deploys, such as the development, test, and production environments in Rails. This method does not scale cleanly: as more deploys of the app are created, new environment names are necessary, such as staging or qa. As the project grows further, developers may add their own special environments like joes-staging, resulting in a combinatorial explosion of config which makes managing deploys of the app very brittle.

In a twelve-factor app, env vars are granular controls, each fully orthogonal to other env vars. They are never grouped together as “environments”, but instead are independently managed for each deploy. This is a model that scales up smoothly as the app naturally expands into more deploys over its lifetime.

I did not understand this at first. I mean, how can you not group the environment variables? At the end of the day you have to have the list of values written down somewhere, so that you can create a release for a particular environment (prod/dev/etc). It’s turtles all the way down, right?

I started imagining some software that would serve as a manager of the env variables, so that you can modify their values per release or something, but it couldn’t be what the author of the manifesto had in mind, because you can achieve the same thing while modifying the files that define the values for a release. No need for additional technology – if anything, this should be against the manifesto. And even if it was about the software, you will for sure group the variables, the whole idea of having particular values for a release is grouping.

And then it hit me – it’s not about storing the variables in groups, it’s about using groups in code.

A good idea

Let’s imagine you have some service that is expected to use a local filesystem when run locally and a 3rd party HTTP API when used on actual servers. We could write it this way:

class SomeService {
  public run() {
    if (process.env['APP_ENV'] === 'LOCAL') {
       this.runUsingLocalFs();
    } else {
       this.runUsingHttpApi();
    }
  }
}

Seems fine at first. You got the service behaviour designed exactly how you wanted.

The APP_ENV variable needs to be defined properly in all environment definitions: local environment, dev environment, prod environment, you name it. So you add these APP_ENV=PROD or APP_ENV=LOCAL to some YAMLs or other .env files to make it work correctly.

With time you create more services that need to rely on some fallback behaviour, for example a logger service, that pretty-prints when it’s useful, otherwise it creates standardized JSON logs.

class Logger {
  private print(message: string) {
    if (process.env['APP_ENV'] === 'LOCAL') {
       this.prettyPrint(message);
    } else {
       this.jsonPrint(message);
    }
  }
}

It gets messier…

Then a couple more services that behave in a similar way. Maybe there are some that behave the same way on more than one environment

if (['LOCAL', 'DEV'].includes(process.env['APP_ENV'])) {
  ...
} else {
  ...
}

You decide to create some utils, maybe an enum const1 not to make a typo in the environment name:

export const APP_ENV = {
  LOCAL = 'LOCAL',
  DEV = 'DEV',
  PROD = 'PROD',
} as const;

or maybe some functions to make it more consise:

export function isLocalOrDevEnv() {
  return [APP_ENV.LOCAL, APP_ENV.DEV].includes(process.env['APP_ENV']);
}
export function isLocalEnv() {
  return [APP_ENV.LOCAL].includes(process.env['APP_ENV']);
}

Adding a new environment? Ok, sure, just extend the APP_ENV. And the utils. And make sure that all the services are running correctly when they APP_ENV contains a value that was not decided on previously.

…and messier

What if you want to run some integration tests that call SomeService? You either have to define APP_ENV as LOCAL or something else, to choose the implementation you want.

Choosing the APP_ENV also changes the behaviour of other N services, that now have to behave as if they were on some particular environment. Trying to get the desired behaviour of one service that relies on APP_ENV is now a game of whack-a-mole, or a minesweeper – you change one setting and then you see multiple things changed.

I believe this is the grouping of which the manifesto tried to warn us.

Solution

If the services were implemented with separate config flags, like this:

class SomeService {
  public run() {
    if (process.env['SOME_SERVICE_LOCAL_FS']) {
       this.runUsingLocalFs();
    } else {
       this.runUsingHttpApi();
    }
  }
}

class Logger {
  private print(message: string) {
    if(process.env['LOGGER_PRETTY_PRINT']) {
       this.prettyPrint(message);
    } else {
       this.jsonPrint(message);
    }
  }
}

then you avoid the issues mentioned above.

You still need to decide what is the default behaviour (should it be some particular implementation or throwing errors when the configuration is undefined) – I guess there are some best practices as well. But at least you are not grouping unrelated features with an environment name.


  1. no enums please ↩︎