Cool Chef Tricks: Install and Use RubyGems in a Chef Run

One of the 9 Things to Like About Chef is that "it uses Ruby as the configuration language." We talk about that in that blog post, and we mention it on the wiki, but at the time we didn't have many examples. This post is the first in a series about cool things that Chef can do with its Ruby DSL.

To start the series off, let's look at installing a RubyGem package with Chef, and then using that Gem later on in the Chef run.

Ruby Red photo by Orbital Joe
Our Rails Infrastructure example sets up RadiantCMS from the GitHub repository with a MySQL backend. We need to make sure the database is created properly, but only if it doesn't exist. Using the mysql RubyGem, we can do this elegantly in Ruby, and show off a cool feature of Chef's.

First of all, in the client recipe from the Opscode MySQL cookbook, we install the RubyGem, mysql.

r = gem_package "mysql" do
  version "2.7"
  action :nothing
end

r.run_action(:install)

This looks suspiciously like any other resource in Chef, but notice that we assign the resource to a variable, 'r', and the action is :nothing. This allows us to use the Chef::Resource#run_action method to trigger the installation. This will actually happen before the node's configuration is converged, that is, the installation occurs first. The experienced Rails developer will know that the mysql Gem requires compilation, so a C compiler and libraries are required, but if the Chef Installation instructions are followed, we already have that.

Once the RubyGem is installed this way, we need to tell Chef to reload the cache of available gems, and then we can require the mysql gem. This goes straight in the recipe too.

Gem.clear_paths
require 'mysql'

Now with that out of the way, we can use the actual gem within a resource. The following is from the Rails stack, database site-cookbook, where we create the actual database for the railsapp – radiant in this case.

execute "create #{node[:railsapp][:db][:database]} database" do
  command "/usr/bin/mysqladmin -u root -p#{node[:mysql][:server_root_password]} create #{node[:railsapp][:db][:database]}"
  not_if do
    m = Mysql.new("localhost", "root", @node[:mysql][:server_root_password])
    m.list_dbs.include?(@node[:railsapp][:db][:database])
  end
end

Here we create the 'm' Mysql object, and then list the databases on the server, checking whether the one we want is there. We could even use the Mysql gem to handle creating the database, but for our purposes here, the mysqladmin command is sufficient. Here's log output showing this in action:

[Fri, 22 May 2009 14:10:31 -0600] INFO: Starting Chef Run
[Fri, 22 May 2009 14:10:32 -0600] INFO: Storing updated cookbooks/database/recipes/default.rb in the cache.
[Fri, 22 May 2009 14:10:34 -0600] INFO: Installing gem_package[mysql] version 2.7
[Fri, 22 May 2009 14:10:39 -0600] INFO: Ran execute[apt-get-update] successfully
[Fri, 22 May 2009 14:10:44 -0600] INFO: Ran execute[create radiant_live database] successfully
[Fri, 22 May 2009 14:10:44 -0600] INFO: Chef Run complete in 7.237168 seconds

In the recipes, the execute[apt-get-update] resource comes before the gem_package[mysql], yet the gem is installed first. Cool!

Another example can be seen in full, in the Rails stack 'application' site-cookbook. We do the same thing with the excellent chef-deploy gem by Ezra Zygmuntowicz of Engine Yard, which provides a 'deploy' resource.

r = gem_package "chef-deploy" do
  source "http://gems.engineyard.com"
  action :nothing
end

r.run_action(:install)

Gem.clear_paths
require "chef-deploy"
# [ .. snip .. ]
deploy "/srv/#{node[:railsapp][:app_name]}" do
# [ .. snip .. ]
end

This is just one of the cool things that you can do with Chef to make programming your infrastructure better. We will post more tips and tricks like this in the future.

Author Joshua Timberman

Joshua Timberman is a Code Cleric at CHEF, where he Cures Technical Debt Wounds for 1d8+5 lines of code, casts Protection from Yaks, and otherwise helps continuously improve internal technical process.