Rails configuration madness

General configuration is always needed in an application. Sometimes this can range from "On what domain I'm I running?" to "Where is that other service located?". Providing a Rails application with configuration during boot can be quite confusing so let's go over our options.

Rails config.x blackhole

Guess what our favorite framework has support to save configuration in a global variable! During initialization you can define (in application.rb, or development.rb/production.rb) some properties on the config.x object.

config.x.payment_types = [:credit_card, :union_pay]  
config.x.payment_processing.retries  = 3  

These are then available through your application on Rails.configuration, giving above example we get the following very short keys

Rails.configuration.x.payment_types.schedule # => [:credit_card, :union_pay]  
Rails.configuration.x.payment_processing.retries  # => 3  

Rails config_for

Another option is to load a YAML file using Rails.application.config_for(:yaml_file_name). The YAML file should be located in the config dir. The cool part is that config_for knowns about the environment you are running and automatically loads the correct variables. Basically works exactly the same as database.yml.

development:  
  payment_types: [:credit_card]
  payment_processing:
    retries: 1
test:  
  payment_types: [:stub_card]
  payment_processing:
    retries: 1
production:  
  payment_types: [:credit_card, :union_pay]
  payment_processing:
    retries: 3
# In application.rb for example
config.x.yml_config = Rails.application.config_for(:payment_config)  

In development Rails.configuration.x.yml_config[:payment_types] will hold [:credit_card]. While in production it will hold [:credit_card, :union_pay]

ENV variables

The most popular gem is dotenv. It allows to define ENV variables in a file and it will load them. You can have multiple .env files per environment (.env.test).

export S3_BUCKET=YOURS3BUCKET  
export SECRET_KEY=YOURSECRETKEYGOESHERE  

Then they are available in the ENV variable like: ENV['S3_BUCKET'].

And that's why they all suck

Option 1 & 2 are related since they both use config.x blackhole. The YAML file is allright and comes in very handy but why do I have to type Rails.configuration.x.yml_config.payment_types? Thats way too long the same goes for secret.yml, accessing a value ends you up in Rails.secrets.whatever not really user friendly.

Then option 3: ENV variables, I have so many issues with them let me create a small list:

  1. Typos: They happen and with ENV variables you have no clue if you made a typo it will be nil anyway.
  2. Security: I can create a gem that reads all your ENV variables and send them to me. Yea ENV variables are shared between forks and subprocesses.
  3. Unicorn: You need to completely reboot your unicorn if a ENV variable gets changed. A hot restart won't work since it forks the master process to create a new master process. That means downtime.
  4. Scrubbed Environments: For security reasons, cron and monit don't start processes with ENV variables. You can work around it but not really recommended.

Our current solution

Last few weeks I had some fun on some smaller projects which all used dotenv and converted them to use YAML files but allow accessing the values more easily than Rails currently provides.

require 'ostruct'  
module ZooConfig  
  # Allows to create deep nested open structs
  # and access them using the keys as methods without using
  # respond_to_missing
  class Config < OpenStruct
    def initialize(hash = {})
      @table = {}
      @hash_table = {}
      hash.each do |k, v|
        @table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
        @hash_table[k.to_sym] = v
        new_ostruct_member(k)
      end
    end

    def to_h
      @hash_table
    end
  end

  # Wrapper around `Rails.application.secrets`
  # @return [ZooConfig::Config] secrets
  def secrets
    @secrets ||= Config.new(Rails.application.secrets)
  end

  # Wrapper around `Rails.application.config.x.app_config`
  # @return [ZooConfig::Config] config
  def config
    @config ||= Config.new(Rails.application.config.x.app_config)
  end

  # Wrapper around `Rails.application.config.x.app_config[:hosts]`
  # @return [ZooConfig::Config] hosts
  def hosts
    @hosts ||= config.hosts
  end
end  

This file is located in lib and in our application.rb we require the lib/zoo_config.rb file and the associated YAML file.

# application.rb
require './lib/zoo_config.rb'  
module ZooKeeper  
  extend ::ZooConfig
  class Application < Rails::Application
    # Other configuration goes here
    config.x.app_config = Rails.application.config_for(:config)
  end
end  

And finally I can easily access my secrets using ZooKeeper.secrets.s3.bucket.client_id instead of Rails.configuration.x.app_config[:s3][:bucket][:client_id]. My sanity is back at least. In the zoo_config file I've also added a shortcut for Rails.application.config.x.app_config[:hosts] so I can easily call my hosts list from the YAML file using ZooKeeper.hosts. An added bonus is that if you make a typo somewhere your application will blow up instantly and you know where the problem is.

Conclusion

I like this way for keeping configuration, it's somewhere between the Rails standard way with a bit of extra sugar. Writing the blog and doing some research let me to the following Gem (named Global) that basically does the same. It uses method_missing and everything is accessible using Global. It also has support for JS configuration which seems pretty cool.

Comments powered by Disqus