Custom Configuration for Rails Environments
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.
Comments