Logo Devoh

Configuring Rails Apps

Applications today rely on an increasing number of external services, each requiring their own set of API keys and configuration parameters. There are several different approaches you can take to organizing these settings in a Rails app. Here's the process I went through in figuring out how to best store those configuration values so they stay organized and easy to manage.

config/

Since Rails is all about convention over configuration, the config directory is probably a good place to start. It's obviously where the configuration is supposed to go, but there a handful of places under that directory to choose from.

config/application.rb

The config/application.rb file is the starting point of the Rails configuration process. There are a bunch of framework-level parameters in here that are generated along with the rest of the Rails app, but it's probably best to leave this for those types of settings.

class Application < Rails::Application
  config.encoding = 'utf-8'
end

If you can't configure it off the #config method, I'd argue that it doesn't belong here.

config/environments/*.rb

There are files under config/environments for each of the default environments in Rails (development, test, production). This breakdown provides us with a natural segregation among the various environments for settings that differ among them. For instance, in our staging environment we could use a service's sandbox credentials, so we don't end up polluting the production system or inadvertently emailing users while testing.

Like config/application.rb, though, these files are already full of Rails configuration parameters. While they may be a somewhat logical place to put other environment-specific settings, they've always felt like a better place for framework-level configuration to me. Using them for things like external service configuration is a bit awkward.

config/initializers

Creating separate files under config/initializers for each library and service is a good alternative location. Segregating by service keeps the configuration well-organized and easy to maintain, but for settings that change among environments, you'll end up with a lot of conditional blocks like this:

if Rails.env.production?
  # production configuration
elsif Rails.env.staging?
  # staging configuration
else
  # local configuration
end

One common way around this is to use a YAML file under config. The initializer can then load and parse this YAML file, using the values under the key that matches the environment name.

config = YAML.load_file(Rails.root.join('config/redis.yml'))[Rails.env]
Redis.current = Redis.new(host: config['host'], port: config['port'])

This brings with it its own set of issues, though, including the fact that you still have to either store your credentials in the repository or do the symlink shuffle during deployment.

Environment Variables

This is where environment variables come into play. They're great because they let you write your configuration logic once, but also allow different values to be used depending on the environment or even a specific server instance. If you've ever used Heroku, you may have noticed that their platform makes heavy use of environment variables for configuring both the platform itself and most of the add-ons.

Airbrake.configure do |config|
  config.api_key = ENV['AIRBRAKE_API_KEY']
end

Another handy side effect I've realized is that, especially with Heroku, you can easily change the value of these variables and restart the server to change all sorts of settings without having to deploy a new version of the app. I've found myself extending this concept even beyond service configuration and into business rules. For instance, storing constant values as environment variables lets you tweak your business rules after the app has been deployed.

Say I'm launching a beta, and there's a fixed waiting period before users are sent their invitation to join.

class Invite
  WAITING_PERIOD = ENV['WAITING_PERIOD'].to_f.days
end

I might start out with a value of zero to let users in right away, but if the beta starts picking up too many users, I can quickly dial back the load by increasing the wait time with a simple Heroku command:

heroku config:set WAITING_PERIOD=2

Unifying the Settings

This gets us pretty far. We now have a good place to store our configuration files and a nice method for storing values that may change, but I found myself with a growing number of these environment variables scattered throughout the config and now app directories. I began to add documentation to the README in the root of the app, but this quickly began to get out of hand, and I felt like I was constantly at risk of leaving something out that the next developer who came along would be stuck scratching his head over.

So, I starting thinking about how to centralize these configuration parameters in a way that not only increased their proximity to one another, but also acted as self-documentation of sorts. Initially I thought a library like Redis::Settings might be helpful for this, but the more I thought about, the less it made sense to me to have to rely on a server for storing these settings.

This led me to just try doing the simplest thing that came to mind: a module full of module methods, with each method corresponding to a single configuration parameter.

module Settings
  extend self

  def app_name
    ENV['APP_NAME']
  end
end

Now, wherever I had previously referenced ENV['APP_NAME'], it was just a matter of replacing it with Settings.app_name. Not only does it read better, but it's now a method so I can easily swap out the source of that bit of configuration data with anything I like (even Redis!) if the need should arise.

The WAITING_PERIOD example above can even encapsulate the type conversion:

module Settings
  extend self

  def waiting_period
    ENV['WAITING_PERIOD'].to_f.days
  end
end

But I still needed to be able to handle a couple other requirements:

  • default values for when the environment variable isn't defined
  • an indication of which values are required in specific environments

The first is easy enough in Ruby. I chose to accomplish it through the use of a method argument default value.

module Settings
  extend self

  def waiting_period(default=0)
    (ENV['APP_NAME'] || default).to_f.days
  end
end

The second, conditionally requiring some values to be defined based on the current environment, is simple enough, as well.

module Settings
  extend self

  RequiredValue = Class.new(StandardError)

  def app_name
    ENV['APP_NAME'] || required_in_production
  end

  def required_in_production
    raise RequiredValue if Rails.env.production?
  end
  private :required_in_production
end

With this in place, the application will refuse to start without the APP_NAME environment variable.

The Result

Using these techniques provides

I've been using this technique for all sort of magic values, and I'm happy with the results. It has satisfied all my initial requirements, and surprised me with some added benefits along the way, like the ability to encapsulating additional logic around the values.

I'm still using a single, monolithic module to encapsulate all the application's configuration, though, and at times this feels messy. I've resorted to using method prefixes like app_ and smtp_ to keep things tidy, but this might be a good indication that additional modules would be useful.

and tagged with ruby