#85 YAML Configuration (revised)
- Download:
- source codeProject Files in Zip (59.4 KB)
- mp4Full Size H.264 Video (23.4 MB)
- m4vSmaller H.264 Video (11.7 MB)
- webmFull Size VP8 Video (12.6 MB)
- ogvFull Size Theora Video (30.2 MB)
Below is a screenshot from a blogging application with multiple articles. If we try to edit an article we’ll see that the edit page is protected by HTTP Basic authentication and we’ll need to enter a user name and password before we’re allow to access it.
To add this authentication we use the http_basic_authenticate_with
method in the ArticlesController
.
http_basic_authenticate_with name: "admin", password: "secret", except: [:index, :show]
The problem with this is that we’re hard-coding the name and password directly into the code. This means that these values are stored in the source code repository which means that the production password will be shared with all the developers and this isn’t very secure. A quick fix for this is to store these values in environment variables.
http_basic_authenticate_with name: ENV["BLOG_USERNAME"], password: ENV["BLOG_PASSWORD"], except: [:index, :show]
Doing this moves these values outside of the code base so that they’re not stored in the source code repository. It does mean, though, that we’ll have to set these environment variables somewhere. We could do this before we run each Rails command but this would be awkward to do.
$ BLOG_USERNAME=admin BLOG_PASSWORD=secret rails s
Alternatively we can set these values through our shell profile file like this:
export BLOG_USERNAME="admin" export BLOG_PASSWORD="secret"
This works but as our application’s configuration becomes more complex we’ll need to keep this file up-to-date and so will each developer working on the application. Instead we’ll keep the configuration within the app in a similar way to how the database.yml
file works. We’ll create another YAML file in the config
directory for storing this configuration data and put the username and password data into it.
BLOG_USERNAME: "admin" BLOG_PASSWORD: "secret"
Of course we don’t want to store this file in the source code repository as this would mean that the username and password were stored again. As we’re using Git to store the source code we can easily do this by adding it to the files listed in .gitignore
.
$ echo /config/application.yml >> .gitignore
When we do something like this it’s a good idea to create a similar file as an example that we do store in the repository.
BLOG_USERNAME: "xxxxx" BLOG_PASSWORD: "yyyyy"
It’s also a good idea to mention this in the project’s README file. We have a list of setup instructions in this file and we’ll describe how to copy the example file here.
# RailsCasts Example Application Run these commands to try it out. ``` bundle cp config/application.example.yml config/application.yml rake db:setup rails s ``` Requires Ruby 1.9.2 or later to run.
We now have a place to put private settings but how do we load them into our application? We could do this inside an initializer file but other initializers might need to read these settings so it’s better to load them earlier. We’ll load them inside the application.rb
file which is where the Rails framework is loaded in along with the various gems. We’ll load the YAML content from the file which will give us a hash. We could store this hash in a constant but as we’re already using environment variables in our application we’ll update the environment variables with values from the hash.
ENV.update YAML.load(File.read(File.expand_path('../application.yml', __FILE__)))
After we restart our application it will work just like it did before but now the user name and password aren’t stored in our source code repository.
Adding Other Values
Now that we have a central place to store application settings we can think about moving some other things into it such as the secret token which is defined by default in an initializer file. If our application is open-source we don’t want to make this value publicly available so instead we’ll move it into our YAML file and read it from the environment.
Blog::Application.config.secret_token = ENV["SECRET_TOKEN"]
BLOG_USERNAME: "admin" BLOG_PASSWORD: "secret" SECRET_TOKEN: "024e1460a4fb8271e611d0f53811a382f1f6be121..."
Another thing we could move here is the host name that’s used when sending email. In the development.rb
file we set the host in the default_url_options
and this is a common requirement for applications that need to send email.
config.action_mailer.default_url_options = {host: "localhost:3000"}
Other developers might want this to be set differently as they might be using Pow or another developer server so we should move this value into our YAML file.
config.action_mailer.default_url_options = {host: ENV["MAILER_HOST"]}
We can do something similar for the test and production environments. These set the same option but to a different value. If we move all of these and set them in application.rb
file we can have separate configuration options for each different environments. What we want is for our application.yml
file to end up looking like this:
BLOG_USERNAME: "admin" BLOG_PASSWORD: "secret" SECRET_TOKEN: "024e1460a4fb8271e611d0f53811a382f1f6be121..." development: MAILER_HOST: "localhost:3000" test: MAILER_HOST: "test.local" production: MAILER_HOST: "blog.example.com"
Here we’ve defined different settings for each environment. To use these we’ll need to adjust the application.rb
file as the ENV.update
command we use there won’t take the current environment into consideration. We’ll replace that line with this:
config = YAML.load(File.read(File.expand_path('../application.yml', __FILE__))) config.merge! config.fetch(Rails.env, {}) config.each do |key, value| ENV[key] = value.to_s unless value.kind_of? Hash end
Now we set the result of loading the YAML file to a local variable. We can then merge in the hash of settings for the current Rails environment, if there is one. We then loop through each of these and set an environment variable for each key to the value converted to a string as environment variables are picky and expect a string value. Note that we don’t do this if the value is a hash; this means that this code won’t try to set the full environment hashes to environment values. When we restart our application it starts up so it looks like our changes have worked.
An Alternative Approach
A Ruby gem called Figaro is available which takes much the same approach that we have in this episode. If we don’t want to create the YAML-loading code from scratch we can use this instead. The README for this gem mentions some good reasons for storing Rails apps’ settings in environment variables. Doing this is especially useful if we’re using Heroku for deployment. There are, however, also some downsides to doing it this way which it’s good to be aware of. First of all it’s important to understand that environment variables must be set to a string value, which is what we did in our solution. Doing this can cause some unexpected results, though, if we’re trying to store things like boolean values. Trying to store false will store the string “false” which is a truthy value and this might cause unexpected behaviour. We can remove the to_s conversion but this will raise an exception if we pass in an integer or some other unquoted value through the YAML file.
Another thing we need to look out for are conflicts with existing environment variables. If we used USER
instead of BLOG_USERNAME
to store the name of the current user this would clash with the existing USER
environment variable that stores the name of the currently logged-in user. Similarly if we use HOST
instead of MAILER_HOST
this will also overwrite an existing environment variable. To avoid this it’s a good idea to prefix each variable name with the name of our application, although this can get a little messy and so if we’re not going to deploy our application to Heroku it’s better to avoid storing application settings in environment variables. We can instead store them in a constant.
CONFIG = YAML.load(File.read(File.expand_path('../application.yml', __FILE__))) CONFIG.merge! CONFIG.fetch(Rails.env, {}) CONFIG.symbolize_keys!
Now we don’t need to worry about namespacing or conflicts in our YAML files and we can use lower-case keys which are easier to read.
blog_username: "admin" blog_password: "secret" secret_token: "024e1460a4fb8271e611d0f53811a382f1f6be121..." development: mailer_host: "localhost:3000" test: mailer_host: "test.local" production: mailer_host: "blog.example.com"
Now, wherever we use one of these settings in our application we can use CONFIG instead of ENV and use a symbol as the key.
http_basic_authenticate_with name: CONFIG[:username], password: CONFIG[:password], except: [:index, :show]
One thing we need to remember is that when we add or change a setting in our YAML file we also need to update the example YAML file that’s stored in the source code repository so that it stays consistent.