This is a podcast episode transcript. Visit the podcast website for the episode, show notes, and other freebies.
Hello friends, it’s me Adam. Welcome back to Small Batches. I introduced the 12 factor app in the previous episode along with areas where I think it may be improved. I’m calling these improvements the "12.1 factor app". So new features and fixes, but no breaking changes; more like clarifications. I’m going to cover these in coming episodes. Enough preamble for now. On with the show.
The 12 factor app states that applications should read config from environment variables. It implies separation of code and config. That’s about it, but there’s good bones here. I want something bigger from this factor. Specifically that applications may be deployed to new environments without any code changes. This requires a few additions:
- Configure the process through command options and environment variables
- Prefer explicit configuration over implicit configuration
- Use a dry run option to verify config sanity
These points force applications to be explicit in configuration, which in turn requires engineers to take more responsibility for bootstrapping the process. This has proven to be a good thing in my experience.
Consider the first point regarding command line options and environment variables. Developers interact with command line tools every single day. There’s a standard interface for passing flags: command line options. You’ve likely used
curl -X ,
grep -E, or
mysql -u. These tools may even use values from environment variables when command lines options are not provided. This is wonderful because processes may be configured globally with environment variables then overridden in specific scenarios with command line options.
This simple interface also supports another common use case: looking up configuration options. Running a command followed by
-h typically outputs a usage message listing all command options. How many times have you struggled to learn which configuration files or environment variables are required to start a service developed by other engineers in your company? Now compare that to how many times you have struggled to find all the options to the grep command? There is no struggle because
grep --help tells you everything. On the other hand, you’re left hoping that your team members put something in the
README or on confluence.
Moving on to the second point. I’ll explain this by contrasting software produced by two ecosystems.
Rails applications use a mix configuration practices. They may use environment variables but there’s also a a mix of YAML files and environment specific configuration files (such as
staging.rb). Internal code uses a preset number of environments (namely
development) to implicitly change configuration. Deploying to a new environment requires creating new configuration files and/or updating YAML files. Starting a rails application requires running the
rails command. As a result, developers are disconnected from the code that bootstraps application internals then starts a web server.
On the other hand, consider software produced by the go ecosystem. It’s more common to write a main method that configures everything through command line options. In this case there is no need for extra configuration files or implicit configuration based on environment names since the concept is irrelevant here. Naturally this requires developers take more responsibility, but as I said early on, it’s worth it in the end. Configuring these applications is easier to grok as well as deploying them to a variety of environments. That’s what the 12.1 factor app is going for.
The command line interface approach enables DX improvements too. One of my pet peeves is when a process starts then fails at runtime due to some missing configuration options. This grinds my gears because developers devote huge effort to validating user input through web forms or API calls but tend to neglect configuration validation entirely! Plus, it’s just frustrating to learn which values are required through runtime errors. The 12.1 factor app can do better than this.
The 12.1 factor app will fail and exit non-zero if any required configuration value is missing. The main method that processes command line options and environment variables makes this possible. Does the process require connection to a database and no
DB_URL provided? Then Boom! Error message and exit non-zero. The goal is to make it impossible to start the process without sane configuration.
Failing with a non-zero exit status integrates nicely with deployment systems. Recall that a 12 factor "release" is the combination of code and config. Therefore it’s possible for a config change to result in a broken release. Given that 12.1 factor apps fail fast, it’s possible for the deployment system to recognize the failed release then switch back to the previous release. Contrast this was a "fail later" approach. The release may be running but failing at runtime. This looks OK from a deployment perspective since the release started, but it’s totally broken from the user’s perspective. The 12.1 factor app easily avoids this scenario.
The "fail fast" approach catches simple user errors such as all values provided. However that only solves part of the problem. Provided values are not necessarily correct. Here’s an example. Say the application requires a connection to Postgres, so the user sets
POSTGRESQL_URL. However the application cannot connect to the server for any reason. It could be networking, mismatched ports, or an authentication error. Whatever the reason the result is the same: no database connection thus a nonfunctional application. This would cause downtime if deployed to production. I can’t tell how how many times this has happened to me for legitimate reasons or less so (like mistyping a hostname or specifying the wrong port).
My point is this type of error may be eliminated by simply trying to use the provided configuration before starting the process. The idea here is to use a "dry run" mode to check these sort of things. I’ve used the dry run mode to check connections to external resources like data stores or that API keys for external APIs are valid. This aligns nicely with the "trust but verify" motto. It’s simple. At the end of the day developers make mistakes. It’s our job to ensure those mistakes don’t enter production.
Alright. That’s enough for the 12.1 config factor. Here’s a quick recap:
- Configure your process through command line options and environment variables
- Fail fast on any configuration error
- Use a "dry run" mode to verify as much config as possible
- Prefer explicit configuration over implicit configuration based on environment names
Well what do you think of these practices? Have you done anything like this before, if so how did it turn out? Hit me up on twitter at smallbatchesfm or email me at firstname.lastname@example.org. Share this episode around your team too. It’s great reference material for the "new service checklist" or "best practices" section on Confluence you’re always trying to write.
Also go to smallbatches.fm/6 for show notes. I’ll put a link to my appearance on the Rails Testing Podcast where I talk about this topic in more technical detail. If this episode piqued your interest, then definitely check that one out. We talk about preflight checks and smoke tests.
Alright gang. That’s a wrap. See you in the next one. Good luck our there and happy shipping!