bkeepers/dotenv — pull request #131 by bkeepers (2014-08-15 14:09:01+00:00)
Title: Proposal: Envfile

The GitHub.com codebase already has a pattern for managing configuration, but I am _finally_ working on moving it to be completely backed by environment variables. In the process, I find that I'm writing a lot of boilerplate code for setting defaults and casting values. While the result is an application that is easier to configure and more portable, I'm not sure that the configuration is easier to understand.

This got me thinking about a way to make application configuration more explicit, consistent, and self-documenting. I'd like to propose adding support for an `Envfile`.

`Envfile` is a manifest inspired by `Gemfile` and `Rakefile` which explicitly defines the required configuration for an application, sets defaults, validates required variables, and provides accessors for reading the values from `ENV`.

## string

A `string` is the simplest variable that can be defined.

```ruby
desc "The main external hostname."
string :host_name, :default => "example.com"
```

This defines a reader on `ENV.host_name` (or `Rails.application.config.host_name` for a Rails application), returning the default value if `ENV["HOST_NAME"]` is not set.

## boolean

```ruby
desc "Boolean attribute specifying whether the site is in SSL mode."
boolean :ssl, :default => true
```

This defines a reader `ENV.ssl?` and cast the values "0", "1", "true", or "false" into boolean values in Ruby.

## integer

`integer` will validate and cast environment variables to integers.

```ruby
desc "The max duration (in seconds) a request can take before it times out."
integer :request_timeout, :default => 10
```

## `:required`

The `:required` option can be used to enforce that a variable be defined by the environment.

```ruby
desc "A PEM of the encryption key used to encrypt sensitive data."
string :encryption_key, :required => true
```

When the `Envfile` is loaded, it will raise an exception if the required keys are not set.

```
ArgumentError: ENCRYPTION_KEY must be set in `ENV`
```

## `:default`

The `:default` option can make use of variable substitution to use existing environment variables.

```ruby
desc "The host name where the API is served from."
string :api_host_name, :default => "api.$HOST_NAME"
```

A block passed to a declaration will be called if a variable is not set and can be used to execute logic for setting the default.

```ruby
desc "Is the current environment connected to the public internet?"
boolean :online do
Rails.development? ? google_reachable? : true
end

def google_reachable?
# …
end
```

## group

`group` will put a set of variables under a common namespace.

```
group :elasticsearch do
string :host, :default => "localhost:$ELASTICSEARCH_PORT"

string :port, :default => 9200
end
```

This will prefix all the variable names (for example, `ELASTICSEARCH_HOST`) and define the accessors as `ENV.elasticsearch.host`.

## rake env

All of this then makes it easy to inspect the environment variables available to the application.

```
$ rake env

# The main external hostname.
HOST_NAME=example.com

# Boolean attribute specifying whether the site is in SSL mode.
SSL=true

# A PEM of the encryption key used to encrypt sensitive data. (Required)
# ENCRYPTION_KEY=

# Is the current environment connected to the public internet?
# ONLINE=

# The max duration (in seconds) a request can take before it times out.
REQUEST_TIMEOUT=10

The host name where the API is served from.
API_HOST_NAME=api.$HOST_NAME

ELASTICSEARCH_PORT=9200

ELASTICSEARCH_HOST=localhost:$ELASTICSEARCH_PORT
```

## Why?

So, what's the point of all this? Couldn't the same thing be accomplished with a well documented `.env` file, or a `Config` module in my app with real methods? Those would work, but an `Envfile` has the advantage that:

0. All available configuration is explicitly defined, documented and easily inspectable.
0. All configuration can be overridden by environment variables, conforming to the [Twelve-Factor App](http://12factor.net/config) methodology.
0. The manifest serves as documentation, and since it's executable and used by the application, will always be up to date.

## Status

This is still a work in progress. I haven't tried it in a real application yet, so it might turn out to be a terrible idea. Given my history of insisting that dotenv be used only in development, I'm not convinced that this should be part of dotenv.

I would love feedback in any form.