Writing Your First Chef Recipe
Apr 18, 2011
Chef is an infrastructure automation tool that lets you write Ruby code to describe how your machines should be set up. Applications for Chef vary from configuring complicated multi-node applications, to setting up your personal workstation.
As great as Chef is, getting started can be a bit daunting. It’s worse if you’re not sure exactly what Chef provides, and you’ve never written a lick of Ruby. This was me a few days ago, so I thought I’d write a quick Chef introduction from that perspective. In this tutorial, we’ll be creating a Chef recipe for the popular database Redis.
Before we get started, there are two terms we need to define, recipes and cookbooks. In Chef, recipes are what you write to install and configure things on your machine like Redis, sshd or Apache2. A cookbook is a collection of related recipes. For example, the MySQL cookbook might include two recipes, mysql::client
and mysql::server
. A cookbook might also have a recipe for installing something via package management, or from source. Our Redis cookbook will contain just one recipe, which installs Redis from source.
This recipe is available on github.
Getting Set Up
The first thing you’ll want to do is:
$ git clone https://github.com/opscode/chef-repo.git
This gives us the skeleton of our cookbook repository. Next, we’ll create an empty cookbook:
$ cd chef-repo
$ rake new_cookbook COOKBOOK=redis
Our rake
task created some folders we won’t need for this simple recipe, we’ll remove them:
$ cd cookbooks/redis/
$ rm -rf definitions/ files/ libraries/ providers/ resources/
$ cd ../..
The folders we’ll be looking at are:
cookbooks/redis
cookbooks/redis/attributes
cookbooks/redis/templates/default
cookbooks/redis/recipes
Next we’ll create the files we’ll be editing to create our recipe:
$ touch cookbooks/redis/attributes/default.rb
$ touch cookbooks/redis/recipes/default.rb
$ touch cookbooks/redis/templates/default/redis.conf.erb
$ touch cookbooks/redis/templates/default/redis.upstart.conf.erb
To run and test our cookbook, we’ll be using Vagrant, a tool for managing local virtual machines. Instructions for installing Vagrant can be found here. Create a file called Vagrantfile
in the root of the repository. Edit it to look like this:
Vagrant::Config.run do |config|
"lucid32"
config.vm.box = :chef_solo do |chef|
config.vm.provision "cookbooks"
chef.cookbooks_path = "redis"
chef.add_recipe :debug
chef.log_level = end
end
The two most important things to note here are that we’re telling our VM to use Chef to install Redis, and that we want the log level set to debug.
Now run this to download the Ubuntu 10.04 VM we’ll be using:
# note: this download is roughly 500MB
$ vagrant box add lucid32 http://files.vagrantup.com/lucid32.box
Writing Our Recipe
Now we are set up and ready to start writing our first recipe. We’ll start by looking at cookbooks/redis/metadata.rb
. It records metadata about our cookbook, including other cookbooks it depends on, and supported OS’s. For this tutorial, we don’t need to edit it.
Attributes
Next we’ll look at cookbooks/redis/attributes/default.rb
, which is where we’ll be defining the variable options for installing and running Redis. Edit it to look like:
:redis][:dir] = "/etc/redis"
default[:redis][:data_dir] = "/var/lib/redis"
default[:redis][:log_dir] = "/var/log/redis"
default[# one of: debug, verbose, notice, warning
:redis][:loglevel] = "notice"
default[:redis][:user] = "redis"
default[:redis][:port] = 6379
default[:redis][:bind] = "127.0.0.1" default[
This file gives default values for configuration options. The defaults can be overridden by a specific machine. For example, on your development box you might want the data_dir
to be someplace different. Since it’s just Ruby code, we can also use control statements to change these defaults based on things like the host OS. One of the most powerful parts of Chef is that the attributes we’re defining here will be available to all of our configuration file templates. This means we only have to declare the user
variable once, and it will be used to create a new user, and start Redis running as that same user. We’re programming our config files.
A quick note for the non-Ruby programmers out there, when you see :redis
, this is called a symbol. The short story is that it’s a string just like "redis"
, but is more memory efficient if used more than once. In Python, one of the above lines might look like:
default["redis"]["dir"] = "/etc/redis"
Templates
In Chef we use ERB templates to write our config files. In this recipe we’re using two templates, one for the configuration to redis-server
and the other for upstart
. Upstart is a replacement for etc/init.d/
scripts. Edit cookbooks/redis/templates/default/redis.conf.erb
to look like:
port <%= node[:redis][:port] %>
bind <%= node[:redis][:bind] %>
loglevel <%= node[:redis][:loglevel] %>
dir <%= node[:redis][:data_dir] %>
daemonize no
logfile stdout
databases 16
save 900 1
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
and cookbooks/redis/templates/default/redis.upstart.conf.erb
like:
#!upstart
description "Redis Server"
env USER=<%= node[:redis][:user] %>
start on startup
stop on shutdown
respawn
exec sudo -u $USER sh -c "/usr/local/bin/redis-server \
/etc/redis/redis.conf 2>&1 >> \
<%= node[:redis][:log_dir] %>/redis.log"
The Recipe File
Now it’s time to write the actual recipe. Having little Ruby experience, I’ll have to do some hand-waving in explaining that the following code is both Chef’s DSL, and perfectly valid Ruby code.
The following code is run from the top-down. It uses Chef resources to create a user, make directories, download and compile Redis, and write out the templates.
Edit cookbooks/redis/recipes/default.rb
to look like:
"build-essential" do
package :install
action end
:redis][:user] do
user node[:create
action true
system "/bin/false"
shell end
:redis][:dir] do
directory node["root"
owner "0755"
mode :create
action end
:redis][:data_dir] do
directory node["redis"
owner "0755"
mode :create
action end
:redis][:log_dir] do
directory node[0755
mode :redis][:user]
owner node[:create
action end
"#{Chef::Config[:file_cache_path]}/redis.tar.gz" do
remote_file "https://github.com/antirez/redis/tarball/v2.0.4-stable"
source :create_if_missing
action end
"compile_redis_source" do
bash Chef::Config[:file_cache_path]
cwd EOH
code <<- tar zxf redis.tar.gz
cd antirez-redis-55479a7
make && make install
EOH
"/usr/local/bin/redis-server"
creates end
"redis" do
service Chef::Provider::Service::Upstart
provider :restart, resources(:bash => "compile_redis_source")
subscribes :restart => true, :start => true, :stop => true
supports end
"redis.conf" do
template "#{node[:redis][:dir]}/redis.conf"
path "redis.conf.erb"
source "root"
owner "root"
group "0644"
mode :restart, resources(:service => "redis")
notifies end
"redis.upstart.conf" do
template "/etc/init/redis.conf"
path "redis.upstart.conf.erb"
source "root"
owner "root"
group "0644"
mode :restart, resources(:service => "redis")
notifies end
"redis" do
service :enable, :start]
action [end
Trying Our Recipe
Now that we’ve written our recipe, it’s time to try it out. In the root of your repository, run vagrant up
. This will start the virtual machine and set up Redis using Chef. Once the command finishes, run this:
$ vagrant ssh
$ echo "ping" | nc localhost 6379
$ exit
If all went well, you should have seen +PONG
. If you change something and want to re-run Chef, type vagrant provision
.
When you’re done working, run vagrant destroy
to reclaim your RAM.
Closing Thoughts
Chef is much more powerful than what I’ve presented, but I hope I’ve been able to show how easy it is to get started writing and editing recipes. If you’d like to learn more about Chef, check out the Opscode wiki.
If you like this post, you should follow me on twitter.