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.
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
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)
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?
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?
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
1 acceptable values, but also
off. Thanks, coercible!
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)
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.