Rails Application Configuration Part 1
October 20, 2016
A number of years ago, working in ASP.NET, when I wanted to make add configuration to web applications, I knew of two main options: Web.config, and the database. The former (an XML file) was pretty much only good for database connection strings. Using the database was hardly surprising then. The database was easy for applications to read from, and could be written to (i.e. configuration changed on a running application) with any SQL client.
In Rails we have different options. Rails itself provides several places: application.rb and the
environment-specific files like development.rb and production.rb, as well as initializers under
config/initializers. These are obviously just ruby and lead to storing configuration in singletons,
either Rails.configuration
or a global like $redis
. While where exactly to load different configuration
is an interesting topic, in this post I’m more concerned with where the values come from. The simplest
and most common are:
- Directly in ruby, in production.rb, for example, like:
config.deliver_email_digest = :weekly
- In a YAML file, config/redis.yml for example:
development:
host: localhost
port: 6379
database: 0
production:
host: 10.0.0.17
port: 6379
database: 1
- In an environment variable inside ruby, config/initializers/redis.rb for example:
$redis = Redis.new(host: ENV["REDIS_HOST"], port: ENV["REDIS_PORT"], db: ENV["REDIS_DATABSE"])
- Using ERB inside a YAML file to load an environment variable, like Rails does for secrets.yml:
development:
secret_key_base: abcd
test:
secret_key_base: efgh
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
Directly putting values in ruby like :weekly
from the first example has fallen out of favour, in no small
part due to the influence of the Twelve-Factor App, which also
highlights the issues of managing standalone files (like the above-mentioned config/redis.yml). Not wanting
to commit sensitive database credentials to version control, and the need for more flexible yet simple
configuration combinations do indeed call for different strategies. The author of
that guide, Adam Wiggins, was at Heroku, so it’s natural that he recommends environment variables. Heroku
runs applications in containers which lends itself nicely to this practice. A container runs a single process
and setting the environment for this process is straightforward. Heroku provides both web and cli tools
for setting environment variables, so it’s a great option if you’re running your application there. Docker,
similarly, allows you to set environment variables in your Dockerfile:
ENV POSTGRES_URL="postgres://username:password@10.0.1.15/database"
If you’re not running your application in containers, you can still use environment variables, but it’s not as straightforward, and there are some issues to be aware of. In development, you can run your server like this:
REDIS_URL="redis://localhost:6379/3" rails server
As you can imagine, no doubt, this quickly becomes unmanageable. That’s where the dotenv gem
comes in. It allows you to load environment variables from a simple .env
file. The foreman gem similarly has the capability to load environment variables from the .env
file (or another
file specified in a command-line switch). Using foreman also has the benefit of being able to export your processes into Ubuntu upstart scripts. These scripts will also include environment variables from .env
which is very convenient
since upstart-run processes start with a mostly clean environment.
Another particular challenge is zero-downtime deploys with a forking web server like unicorn. Replacing the running unicorn with a reload doesn’t give you an opportunity to reload environment variables. Workarounds like this do work, but at this point you’re loading environment variables (in production) from a file, so it’s essentially no different than loading your configuration directly from a file instead of from the environment at this point.
If you are using disposable containers on Heroku or with Docker, where updates are done through creating and destroying containers rather than have an existing master process exec a new master process, environment variables make perfect sense. Otherwise you might as well stick to reading local YAML files since you’ll probably need to explicitly reload environment variables from a file anyway.
In part 2, I’ll examine application configuration with distributed key-value stores. In other words, configuration in a database is back, provided it’s not a relational database.