Introducing environmentalist: an intuitive, environment-focused config structure for your rails applications

A couple of months ago, I wrote an article (Better setup for environments in rails) discussing the standard set of changes we make to the config structure of each of our rails apps.

The primary motivation for me to make these changes stemmed from the need to have several deployable environments. The standard set of rails environments (development, test, production) simply just don't cut it for me. It's important for us to be able to deploy to staging, demo and even production-test environments. When including server configurations (e.g. a Passenger config snippet, or a mongrel_cluster config snippet), I've often had to use unique configurations for each deployable environment. Consequently, my config/ directory quickly became polluted with files such as: apache_prod.conf, apache_staging.conf, apache_demo.conf. Furthermore, it also requires special care when deployment comes around.

After manually handling this process for about a dozen apps, I decided to knock out an executable that would take care of this for me. Alas, environmentalist was born!

The basic notion behind environmentalist is that application/server configuration should be centered around environments. Rails has done this to an extent (there is a config/environments directory), but these simply contain a ruby script that ties into the initialization of an application. There's no capacity for defining server/environment configuration outside of the scope of rails.

The gem installs a script that can handle both brand new applications as well as apps that have been in development for any period of time. The gem itself is only about 100 lines of ruby shell code that shifts your files around. In psuedo-code, it looks as such:

  1. Create directories for each environment, e.g. config/development, config/test, etc.
  2. Copy old environment files (config/environments/development.rb) to new structure (config/development/environment.rb)
  3. Remove the old config/environments directory
  4. Install config/postboot.rb # this file just overrides the default paths in Rails that are affected by our structural changes
  5. Update config/environment.rb to include config/postboot.rb immediately after config/boot.rb
  6. Break database.yml out into separate files. config/database.yml becomes config/development/database.yml, config/test/database.yml, etc.

The script is intended to handle the case where any or all of these steps have already been completed. In other words, running the script repeatedly on a particular project should not cause any damage.

Probably the most surprising aspect of this is the fact that I break out a database.yml file for each environment. I've made this argument before, and I encourage you to read it. The gist is this: despite the concern some have in the community about including database credentials in your repository, the fact is if you lock down your box, it doesn't really matter (don't allow access to your database server outside of a closed network, hello iptables!). And it's certainly very convenient to have credentials for your deployable apps right in your config directory. This also allows us to separate our local credentials (development, test) -- which have no business going into the repository -- from our remote credentials. Database.yml problem solved!

For us, this restructuring achieves several other efficiencies. Two other gems I have (capistrano-extensions and passenger-recipes) take advantage of this structure to allow me to more easily create extra deployable environments. SmartLogic has reached a point now where it's extremely helpful for us to deploy demo versions of almost all of our apps. With just a little bit of care taken upfront (and a demo server), we can now get this functionality extremely easily across the board.

This is great, how can I have it?

Well you can grab the gem off of github. I don't actually have the gem hosted there (I use rake to dynamically build my gemspec, and I haven't figured out how to allow github to build my gem without hard-coding version numbers and other values into my .gemspec file....any suggestions would be welcome!). The following steps will install the gem.

$> git clone git://github.com/jtrupiano/environmentalist.git
$> cd environmentalist
$> rake gem
$> sudo gem install pkg/environmentalist-0.1.0.gem

How do I use it?

An executable named "environmentalize" is installed when the gem is installed.

/path/to/my/rails/project$> environmentalize

Or, if you're not in the root of your project:

~$> environmentalize path/to/rails/root

For completeness (and so you don't have to download the source), I wanted to include the contents of postboot.rb:

# Be sure to restart your server when you modify this file.

rails_env = ENV['RAILS_ENV'] || 'development'

env_dir = File.join(RAILS_ROOT, 'config', rails_env)
db_file = File.join(env_dir, 'database.yml')
env_file = File.join(env_dir, 'environment.rb')

unless File.exists?(env_dir)
  puts "#{env_dir} environment directory cannot be found."
  exit(1)
end

unless File.exists?(db_file)
  puts "#{db_file} is missing. You cannot continue without this."
  exit(1) # exit with an error code
end

unless File.exists?(env_file)
  puts "#{env_file} environment file is missing."
  exit(1)
end

# Now, let's open up Rails and tell it to find our environment files elsewhere.
module Rails
  class Configuration

    def database_configuration_file
      File.join(root_path, 'config', environment, 'database.yml')
    end

    # The path to the current environment's file (development.rb, etc.). By
    # default the file is at <tt>config/environments/#{environment}.rb</tt>.
    def environment_path
      "#{root_path}/config/#{environment}/environment.rb"
    end
  end
end
You've successfully subscribed to SmartLogic Blog
Great! Next, complete checkout for full access to SmartLogic Blog
Welcome back! You've successfully signed in.
Unable to sign you in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.