Article summary
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
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