We’re going to talk about factors 3 and 4 together so that they can be examples for one another. This is the second entry in my series about building a 12Factor app using this blog as an example. Configuration
Configuration
If you’re coming from a framework like Rails in Ruby, Nuxt for Javascript, (or even your config.yaml in Jekyll!) you may be used to having some configuration file that contains some schema for your app; unfortunately, that’s not what the 12Factor app means by config. A 12Factor app’s config refers to anything that can potentially change between deployments. The basic tenet of configs is that configuration requires a strict separation from code (emphasis from The 12 Factor app). This may sound confusing in the abstract but practically its reasonably straightforward; for maximum reproducibility, your app should always set important variables at runtime instead of using hardcoded configurations.
A proper 12Factor app should never store important variables as configs in your repo. Not only is this a bad coding practice, but hardcoding keys into your app as variables is a terrible security practice. An excellent way to make sure that you adhere to this policy is to ask yourself, if my code were open sourced right now, at this very instant, would the app be secure? Instead of using hardcoded solutions configs should be environment variables that are easy to exchange between instances of your app. We’ll examine using variables after looking at Step 4, so we have some practical application to explore. Backing Services
Backing Services
Anything that a 12 Factor app consumes over a network is considered to be a backing service. It is important to note that this definition does not distinguish between services on a local intranet or those externally based that must be accessed through a network request. Each backing service is referred to as a resource in this model and can be anything from a database, message system, API, even email or logging services. The idea with backing services is that your app should agnostically consume data from a resource and should be able to operate gracefully if you decided to use another resource tomorrow. Some services may be easier to swap out like your database or logging system and some may not have natural alternatives for you to swap like a social media feed. No matter what service you use your app should handle making a configuration change pointing to a new resource and still be able to function the same way.
Putting them together
Configs and backing services can be very closely tied together, and it often helps to examine them together to get a better idea of how each one works. Let us take a look at a generic CRUD application first and explore building it as a 12Factor app. First, we’ll set up the configurations and then focus on applying a backing store.
After a running rails new blog to set up a new Rails application, we want to set up both the config and the backing services appropriately. Let’s say for example that we know we want to ultimately deploy this app to Heroku like a lot of Rails apps, but we have a slight problem right away with our scaffolding. Rails by default provides the sqlite3 Gem for our database, but Heroku doesn’t work with SQLite. So we know that we will want to pick another database for production and or staging environments outside of a local development environment. The goal then is to configure an app to load and run the same way with each database using environment variables that will not be committed to a git repo.
Now let’s say we have configured a group for production in Gemfile that includes the pg PostgreSQL gem and moved sqlite3 into the development and test groups. Now the correct action would be to set the default password for our database using an environment variable locally and ongoing to Heroku and setting an environment variable for when we deploy to production ( this is why most web hosting sites provide some way to manage local variables that get loaded when running your app). This works great; it keeps our database password out of our codebase while providing it as an environment variable at run time for our new blog application. Even better, we can utilize a different password for our production database in PostgreSQL on Heroku and successfully switch our backing service without touching our codebase.
Now that we have set up the blog with a database lets examine adding images from an external source. Suppose we want to store some pictures for our new blog on Amazon S3 so that they can be easily pushed out to a CDN like Amazon CloudFront. Rails should behave the same way loading images for our blog no matter if locally hosted or at a remote S3 bucket. Newer versions of Rails (>5.2) use a feature called ActiveStorage to support this easily and provide a clear separation between backing services. Using tools like ActiveStorage in a codebase allows us to develop our app using interoperable data stores.
Adding More Complexity
While this basic approach works great, there are also other tools we can use to achieve the same result. Both Ruby and Javascript each have packages called DotEnv based explicitly based on the 12Factor app (each package explicitly mentions the 12Factor app in their README’s). These packages provide a way to read secrets and are added to your .gitignore, so they aren’t exposed to the codebase (for more information on using the DotEnv gem, check out this handy guide). There are also tools like Vault or Consul which can be used to store passwords and keys so which can be safely read and consumed by your app. Instead of relying on keeping a local copy of the variables to be read you can use a service that stores them securely. Depending on your team size it may be more useful to use a commercial third party than manage sharing a set of local .env files.