Writing recipes
As you have already seen, cookbooks provide a way to combine relevant pieces of configuration data such as attributes, templates, resources, providers, and definitions in one place. The only reason these components exist is to support our recipes. Recipes combine resources in a certain order to produce the desired outcome; much in the same way a chef would combine ingredients according to his or her recipe to produce some delicious food. By putting all of these resources together, we can build our own recipes that range from very simple single-step recipes to multistep, multiplatform recipes.
Starting out small
A very basic recipe, as we have discussed before, might only leverage one or two resources. One of the simplest conceivable recipes is the one we used earlier to verify that our Chef-solo installation was working properly:
file "#{ENV['HOME']}/example.txt" do action :create content "Greetings #{ENV['USER']}!" end
Here again, we are combining a single resource, the file
resource, specifying that we want to create the file named $HOME/example.txt
, and store the string "Greetings $USER"
in that file. $USER
and $HOME
will be replaced by the environment variables, most likely the login name of the user that is executing chef-client
and their home directory respectively (unless the environment variables have been tampered with).
Following our goal of idempotence, executing this recipe multiple times in a row will have the same effect as only running it once.
Installing a simple service
Now that we've covered a simple recipe, let's take a look at one that configures the Redis engine and uses supervisord to run the daemon. This recipe doesn't install Redis; instead, it defines how to configure the system to start and manage the service. It does not have any advanced logic, but merely constructs some required directories, builds a configuration file from a template, and then uses the supervisor_service
resource to configure the daemon to run and be monitored, as shown in the following code:
redis_user = node[:redis][:user] redis_group = node[:redis][:group] environment_hash = {"home" => "#{node[:redis][:home]}"} # Create the log dir and data dir [node[:redis][:datadir], node[:redis][:log_dir]].each do |dir| directory dir do owner redis_user group redis_group mode "0750" recursive true end end # Generate the template from redis.conf.erb template "#{node[:redis][:config_path]}" do source "redis.conf.erb" owner redis_user group redis_group variables({:data_dir => "#{node[:redis][:data_dir]}"}) mode 0644 end # Convenience variables for readability stdout_log = "#{node[:redis][:log_dir]}/redis-stdout.log" stderr_log = "#{node[:redis][:log_dir]}/redis-stderr.log" redis_bin = "#{node[:redis][:install_path]}/bin/redis-server" redis_conf = "#{node[:redis][:config_path]}" # Tell supervisor to enable this service, autostart it, run it as # the redis user, and to invoke: # /path/to/redis-server /path/to/redis.conf supervisor_service "redis_service" do action :enable autostart true user "#{redis_user}" command "#{redis_bin} #{redis_conf}" stdout_logfile "#{stdout_log}" stderr_logfile "#{stderr_log}" directory "#{node[:redis][:install_path]}" environment environment_hash end
You will notice that in order to keep the configuration consistent, we reuse a lot of attributes. For example, the beginning of the recipe uses node[:redis][:datadir]
and node[:redis][:log_dir]
to ensure that the directories exist by making use of a directory resource inside of a loop; then, these are used later on to define the supervisor configuration variables (where to write logs) and the template for the config
file (where to store the data). In all, this recipe is composed of four resources: two directories in the loop, one template, and one supervisor service. By the end of this run, it will have ensured the critical directories exist, Redis is configured, and a supervisor configuration file is generated (as well as poked supervisord to reload the new configuration and start the service). Again, running this multiple times, assuming no configuration changes in between runs will put the system in the exact same state. Redis will be configured according to the host properties, and supervisor will run the service.
Getting more advanced
Let's move up and take a look at a slightly more complicated, yet fairly simple, recipe from the git
cookbook that installs the git
client on the host. The cookbook is multiplatform, so let's talk about what it will be doing before it shows you the source. This recipe will be performing the following actions:
- Determine which platform the end host is running on (by inspecting the
node[:platform]
attribute). - If the host is running a Debian-based distribution, it will use the package resource to install
git-core
. - If the host is a RHEL distribution, it will perform the following:
- Include the EPEL repository by pulling in the
epel
recipe from theyum
cookbook if the platform version is 5. - Use the package resource to install
git
(as that is the RHEL package name).
- Include the EPEL repository by pulling in the
- If the host is Windows, it will install
git
via thewindows_package
resource and instruct it to download the file located atnode[:git][:url]
(which in turn pulls from the default attributes or overridden configuration), validate that the checksum matches the one specified bynode[:git][:checksum]
, and then install it; however, this is only if the EXE is not already installed. - If the host is running OS X, it will leverage the
dmg_package
resource to install a.pkg
file from a.dmg
image. Here, the download URL, volume name, package file, checksum, and app name are all attributes that need to be provided. - Finally, if none of the conditions are met, it falls back to the package resource to install the
git
package in the hope that it will work.
Here is the code for this recipe:
case node[:platform] when "debian", "ubuntu" package "git-core" when "centos","redhat","scientific","fedora" case node[:platform_version].to_i when 5 include_recipe "yum::epel" end package "git" when "windows" windows_package "git" do source node[:git][:url] checksum node[:git][:checksum] action :install not_if { File.exists? 'C:\Program Files (x86)\Git\bin\git.exe' } end when "mac_os_x" dmg_package "GitOSX-Installer" do app node[:git][:osx_dmg][:app_name] package_id node[:git][:osx_dmg][:package_id] volumes_dir node[:git][:osx_dmg][:volumes_dir] source node[:git][:osx_dmg][:url] checksum node[:git][:osx_dmg][:checksum] type "pkg" action :install end else package "git" end
One thing we haven't seen yet is the use of the not_if
qualifier. This is exactly what it looks like; if the block supplied to not_if
returns a true
value, the resource will not be processed. This is very useful to ensure that you don't clobber important files or repeat expensive operations such as recompiling a software package.