From Wikipedia we read:
An environment variable is a dynamic-named value that can affect the way running processes will behave on a computer.
Environment variables can be seen as a collection of key/value strings that are accessible for anyone in the current process. If the process forks, the children get a copy of the environment. You can access them using %ENV in Perl, getenv in C, System.getenv in Java, process.env in Node.js or os.environ in Python. All languages I know have a way to access the environment.
A lot of programs use them as a way of universal configuration mechanism. All major OS support environment variables and they are used the same way for every program. In contrast, configuration files hold great differences between OSs, programming languages and frameworks.
Environment variables are a useful configuration mechanism because they don’t require any file to be changed. This means you can enforce the distinction between the artifact and its configuration. Code and configuration have different life-cycles and you want to manage them independently. As a hint, you probably don’t want to store your secrets in the same way and place you have your source code.
For those and other reasons, the twelve-factor app methodology enforces the use of environment variables to configure the applications.
A Global challenge
If you start right away using environment variables in your code, you will learn that it comes with some challenges. The environment collection can be seen as a global variable in our application, and we know that global variables are bad.
On top of that, if you start using environment variables everywhere in your code where you need to configure behaviour, you will soon find a problem: There isn’t a way to have an exhaustive list of the environment variables a program is using. This eventually leads to:
- Variables that nobody knows that are used.
- Variables that mean different things in different parts of the code because of (1).
From your users (or the person deploying an application) perspective, there’s no way to know which environment variables are required or which of them are valid values.
Take point that you don’t have an automatic way to enforce the presence of a specific variable in the environment. This means that you will probably receive empty strings, nulls, undefineds or whatever your language is sending you and you’ll have to manually deal with that every time you need to access a variable.
They will also make more difficult to unit test your code, because they are not an explicit dependency. You will have to mangle with the environment collection inside your test cases, and probably deal with extra cases that don’t really add value.
Getting the best the environment can provide
Those are serious challenges, but nothing that can’t be managed. Being a fan of this way of configuring applications myself, I wanted to share a couple of tips for healthier development:
- Always namespace your environment variables.
- Use environment variables only to populate a config object, then pass it around.
A lots of tools put stuff in the environment, so names like USERNAME, DEBUG or PATH are potential collisions. Prepend a meaningful string for your application at the beginning of all your environment variables (like, MYAPP_USERNAME). It will not only be safer, but it will be easier to identify which variables are relevant to you when inspecting the environment content.
On the other hand, a config object is just an (preferably inmutable) object with a set of well-defined attributes, one for each configuration variable you want to support. That object (or objects) can then be injected into other objects requiring them as a dependency. With this approach, you will solve probably the main challenge when dealing with environment variables: anyone will know upfront which configuration options are available and no one will be able to access a key that does not exist without a fair warning.
Also, if some particular configuration is absolutely required to run the application (for example, a connection string to a database) you can make it fail at the very beginning of your workflow, when initializing the config objects. Your other modules can safely rely on all the configuration to be there with the expected shape, reducing the amount of checks they have to do.
Summarizing: make your life easier and use environment variables to configure your application, but hide them by creating config objects.