Custom Configuration for Rails Environments

3 minute read

Previously, I looked at the simply way of creating Rails stages that shared same configuration with Production by simply importing production.rb into the new stage:

require Rails.root.join("config/environments/production")

This is a good start, however it makes a bad idea, stage conditional code, worse:

if Rails.env.staging? || Rails.env.beta? || Rails.env.production?

This kind of conditional is a bad because it means I have to take on faith that the code works, because it only that only gets exercised in production (or possibly beta). Yes, you can write clever tests to get at most of it, but still you have code that says “Don’t run me, except when it’s important.”

That said, of course you have things that you don’t want to happen outside of production, charging credit cards, posting to social media, launching the missiles. As much as possible, the way to handle these differences by configuration, not code.

Rails already provides an example of this in the form of database.yml:

default: &default
  adapter: sqlite3
  pool: 5
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3
config/database.yml

As you know, Rails reads this file automatically chooses the database connection base on the current Rails environment. Starting with Rails 4.2, the mechanism for this was exposed as the Rails.application.config_for method. It takes a symbol or string which is the name of a YAML file in the rails config directory. Let’s say we had some missile settings in target.yml:

default: &default
  test: true
development:
  <<: *default
  name: Null Island
  latitude: 0
  longitude: 0
staging:
  <<: *default
  name: Bottom of the World
  latitude: 90
  longitude: 0
production:
  name: Top of the World
  latitude: -90
  longitude: 0
  test: false

We can load it as follows (here in development):

Rails.application.config_for(:target)
{
         "test" => true,
         "name" => "Null Island",
     "latitude" => 0,
    "longitude" => 0
}

As you can see, it returns a hash of the config for the current Rails environment. Presumably, our missile API won’t actually launch the missiles when test is set to true. If it does, well we didn’t write it.

We can combine this with a second rails feature, our ability to add arbitrary configuration to the Rails.configuration object. Again starting with Rails 4.2, Rails.configuration is a object you can use to access the Rails configuration. Through the use of method_missing, it allows you to assign anything you want to it:

Rails.configuration.twitter_handle = '@spikex'
Rails.configuration.location = {name: 'Null Island', latitude: 0, longitude: 0}

The combine the two methods in config/application.rb:

class Application < Rails::Application
  config.target = config_for(:target)
end

Now we can access our stage specific configuration anywhere in our code with Rails.configuration.target. I find this a little verbose, so I’ve been know to create a module shortcut of sorts. Create app/lib/app.rb (or lib/app.rb in Rails 4) that contains:

module App
  def self.config
    Rails.configuration
  end
end

Now it’s shortened to App.config.target. Alternatively, you could add this to the module that Rails created for you in config/application.rb.

For another bit for fanciness, use a Map or a Hashie Mash with config_for:

config.target = Map(config_for(:target))
# Or
config.target = Hashie::Mash.new(config_for(:target))

Either would then allow you to use App.config.target.latitude, etc.

A bit of paranoid caution, you can add anything you like to Rails.configuration, but so could future version of Rails, so you might consider tucking things that sounds Rails-y under something like Rails.configuration.my_app. But then again, I don’t.

Finally, I’ve skirted around the issue of security. The obvious use case for this sort of configuration is thing like API keys and other credentials. You can certainly use YAML files to store this information, however, just as with database.yml, you need to keep clear text passwords out of your repo. The are secure ways to approach this, which we’ll cover next time.

Tags: ,

Updated:

Comments