Chef Solo with Capistrano

Not that long ago, I wrote about a standalone Puppet pattern that Mike English and I use in conjunction with Capistrano to provision and manage our server configurations.

While we still make use of Puppet, we’ve also added Chef to our repertoire. Similar to Puppet, Chef allows for a client/server model in which a Chef server stores and hosts a series of definitions, called recipes, which are packaged as cookbooks. While this works really well, it doesn’t always make sense for us to setup a full Chef Server — particularly if we’re just trying to manage a single server running within a customer’s existing ecosystem.

Fortunately, we can run Chef in a standalone mode referred to as Chef Solo. To facilitate the execution of Chef Solo and get the necessary Chef cookbooks and recipes in place, we utilize Capistrano. (It really is a magnificent utility, even if poorly documented.)

The general pattern is:

  1. Bootstrap Chef Solo via Capistrano.
  2. Upload Chef cookbooks and recipes via Capistrano.
  3. Run Chef Solo via Capistrano.

To maintain configuration code, we create a separate git repository for each set of Chef Solo instances that we run. If we happen to be using Chef Solo to manage more than one server (such as a database and application server as a single instance), we make use of Capistrano Multistage to handle running Chef Solo on the individual servers. All cookbooks, recipes, Capistrano configuration files, etc. are stored in this repository.

Depending on the situation, we may package the Chef cookbooks and recipes in the same repository as the application for which Chef manages the servers. This keeps all application, deployment, and configuration information centralized.

For a generic Rails application, our source control repository may look something like this (emphasis on directories used by Chef and Capistrano):


├── Capfile
├── Gemfile
├── Gemfile.lock
├── README
├── Rakefile
├── app
├── bin
├── config
│   ├── deploy
│   │   ├── demo.rb
│   │   ├── production.rb
│   │   ├── sandbox.rb
│   │   ├── server_app.rb
│   │   └── server_db.rb
│   ├── deploy.rb
|   └── ...
├── config.ru
├── db
├── doc
├── features
├── lib
├── log
├── public
├── chef
│   ├── cookbooks
│   │   ├── apache2
│   │   ├── mysql
│   │   └── ...
│   ├── roles
│   │   ├── apache2.rb
│   │   ├── mysql.rb
│   │   └── ...
│   ├── server_app.json
│   ├── server_db.json
│   └── solo.rb
├── script
├── spec
├── tasks
└── vendor

Bootstrapping Chef Solo

The first piece of this pattern is the Chef Solo bootstrap, which actually installs Chef on the target server. The primary tool used to manage Chef in a client/server environment is knife. Knife provides a convenient way to bootstrap Chef clients to connect to a Chef server. We can make use of the same approach to setup Chef Solo, but provide a custom bootstrap template:


bash -c '
<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>

exists() {
  if command -v $1 &>/dev/null
  then
    return 0
  else
    return 1
  fi
}

install_sh="http://opscode.com/chef/install.sh"
version_string="<%= "-v #{knife_config[:bootstrap_version]}" if knife_config[:bootstrap_version] %>"

if ! exists /usr/bin/chef-client; then
  if exists wget; then
    bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh} -O -) ${version_string}
  else
    if exists curl; then
      bash <(curl -L <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh}) ${version_string}
    fi
  fi
fi

mkdir -p /etc/chef
'

We invoke knife via Capistrano and call for bootstrapping with our custom template. We pass in the SSH user and server IP based on variables set in our stage. Knife will then SSH to the target server as the given user, and install Chef:


namespace :bootstrap do
  task :default do
    system("knife bootstrap -d chef-solo -x #{user} --sudo #{server_ip}")
  end
end

After bootstrapping, we can use the Chef Solo pattern to configure and manage our servers individually.

Uploading Cookbooks

When we actually run Chef Solo, we need to upload the Chef cookbooks and roles to the remote server. We use Capistrano to do this more easily. We then use Capistrano to invoke Chef, specifically using the .json file corresponding to the stage we have configured for the target server:


namespace :chef do
    task :default do
        # Tar up Chef Cookbooks and Roles
        system("tar czf 'chef.tar.gz' -C chef/ .")
        # Upload
        upload("chef.tar.gz","/home/#{user}",:via => :scp)
        # Remove existing Chef dir to avoid conflicts...
        run("rm -rf /home/#{user}/chef")
        run("mkdir -p /home/#{user}/chef")
        # Untar new Chef Cookbooks and Rols
        run("tar xzf 'chef.tar.gz' -C /home/#{user}/chef")
        # Run Chef Solo
        sudo("/bin/bash -c 'cd /home/#{user}/chef && #{chef_binary} -c solo.rb -j #{stage}.json'")
        # Clean up
        run("rm -rf /home/#{user}/chef.tar.gz")
        run("rm -rf /home/#{user}/chef")
    end
end

Running Chef Solo

The solo.rb file provides the configuration information to allow Chef to run solo. It points chef to the location of the cookbooks and roles on the local file system (as opposed to on the Chef Server where they would normally be hosted).

Our solo.rb file:


root = File.absolute_path(File.dirname(__FILE__))
file_cache_path root
cookbook_path root + '/cookbooks'
role_path root + '/roles'

The *.json files define what roles (or recipes) actually get applied to each server. For example, server_app.json specifies that the apache2 role should be applied, and server_db.json specifies that the mysql role should be applied.

An example server_db.json file:


{ "run_list": [ "role[mysql]" ] }

(Note that we are just telling Chef that it should apply the mysql role to the server, which is defined in roles/mysql.rb)

When we run Chef Solo via Capistrano, we specify which .json file to use depending on the server we wish to operate upon. While perhaps not its original intention, we use Capistrano Multistage to accomplish this.

Here is an example stage:


set :chef_binary, "/usr/bin/chef-solo"
set :user, "atomic"
server "10.0.0.5", :chef, :no_release => :true
set :server_ip, "10.0.0.5"

Getting Started

You can find a skeleton template depicting this pattern on GitHub: https://github.com/kuleszaj/chef-solo-pattern