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:
- Bootstrap Chef Solo via Capistrano.
- Upload Chef cookbooks and recipes via Capistrano.
- 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
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
endAfter 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
endRunning 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 here
Reference: Chef Solo with Capistrano from our JCG partner Justin Kulesza at the Atomic Spin blog.


