ENVied, or how I stopped worrying about Ruby's ENV

Maybe you're familiar with the twelve-factor methodology. More specifically: the principle of configuring your app using the environment.

It's a simple idea: remove any code from your project that may differ between environments (local, CI, staging, production etc.). Instead, inject these values (e.g. settings for database- and mail-servers, API credentials) via the environment.

While this separation of code and configuration makes it simpler to run your application in different environments. It has some challenges of it's own.

This article looks at some of these challenges. It also shows how ENVied, a gem I wrote, can help you solve these issues.

The problem

Imagine we're working on a Ruby application that we want to be configurable via the environment.

As soon as we introduce an environment variable in our code, we're faced with two issues:

  • How to communicate to other developers that this variable can be used to configure the application?
  • How to effectively use the value of this environment variable in our code?

Is this thing on?

It's one thing to state in the README that an application is configurable via the environment. It's another thing to let users of your code know exactly what knobs they can turn.

Specifically, we'd like other developers to know:

  • what environment variables are required?
  • what environment variables are optional?
  • what, if any, default values are used?
  • how are values interpreted? as string? or as integer, or boolean?

Normally you'll have to dive into the documentation to find this information. With a bit of luck a project even contains a sample bash-script you can tweak and load.

Wouldn't it be nice though, to be have some sort of 'executable documentation' that validates the environment for us?

ENVied to the rescue

ENVied, a gem I wrote, does exactly this.

Let's see how a typical configuration would look like.

ENVied is built around the Envfile, a file in the project-root that lists all variables used:

# Envfile
variable :DATABASE_URL

The following code will validate the environment:

# somewhere during bootup
ENVied.require

In this example it means that execution will only continue if and only if ENV['DATABASE_URL'] is present. If it's not, the application will terminate with an error stating the absence of the variable.

So, not only do we now have a place that lists all possible variables. The environment will be validated on boot-up in a fail-fast manner. Neat!

Provide me maybe

Let's look at optional variables.

Say, our project connects to a Redis-server and we'd like to make this configurable via the variable REDIS_URL.

As Redis-servers are by default reachable via a well-known url, we add it as a default value:

# Envfile
enable_defaults! # defaults are disabled by default
variable :REDIS_URL, :string, default: 'redis://localhost:6379'

Connecting to Redis can now be done like so:

@redis = Redis.new(url: ENVied.REDIS_URL)

By using ENVied.REDIS_URL we either get the value ENV['REDIS_URL'], or the configured default value. If we change the default value (or remove it altogether), the only thing we need to do is change the Envfile.

Accessing the environment via ENVied has more benefits: types.

How to effectively use values from the environment?

The value of an environment variable is either nil or some string. When these strings are read from the environment and turned into integers, booleans and what not, there's often the same boilerplate involved:

# integers
port = Integer(ENV['PORT']) # just #to_i won't work

# booleans
# simple: providing any value for ENV['FORCE_SSL'] means `true`:
force_ssl = !!ENV['FORCE_SSL']

# more advanced:
# only when we provide 'true' will `force_ssl` become true.
force_ssl = ('true' == ENV['FORCE_SSL'])

# dates
launch_date = Date.parse(ENV['LAUNCH_DATE'])

Having this code scattered all over your project is messy and hard to change later on; is 1 an acceptable value for a boolean setting? If we want it to be for all boolean settings, what should be changed? If ENV['PORT'] can't be turned into an integer, when will we find out? At boot-up? Or does such code also exist in our background jobs?

With ENVied these variables would be defined as follows:

# Envfile
variable :PORT,         :integer, default: '3000'
variable :FORCE_SSL,    :boolean, default: '1'
variable :LAUNCH_DATE,  :date,    default: '2014-10-15'

# to get the correct type:
ENVied.PORT # => 1
ENVied.FORCE_SSL # => true
ENVied.LAUNCH_DATE # => #<Date: 2014-10-15 ((2456946j,0s,0n),+0s,2299161j)>

Not only will ENVied.require, as we saw earlier, ensure that the environment contains values for these variables. It will also check if the values are coercible to the defined types.

Note: as for setting boolean variables: not only are true/false and 0/1 acceptable values, but also T/F and on/off. Thanks, coercible!

Groups

Not always are all variables needed. A production environment might have different provisioning needs than a development environment.

This is where groups are useful:

variable :FORCE_SSL

group :production do
  variable :SOME_SAAS_API_TOKEN
end

# to only use FORCE_SSL:
ENVied.require

# to *also* use SOME_SAAS_API_TOKEN:
ENVied.require(:default, :production)

Summary

Hopefully this article gave you an idea what issues can arise when making your Ruby application configurable via the environment. And also how ENVied might help you solve these issues.