Chef:Powerful Infrastructure Automation
上QQ阅读APP看书,第一时间看更新

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:

  1. Determine which platform the end host is running on (by inspecting the node[:platform] attribute).
  2. If the host is running a Debian-based distribution, it will use the package resource to install git-core.
  3. If the host is a RHEL distribution, it will perform the following:
    1. Include the EPEL repository by pulling in the epel recipe from the yum cookbook if the platform version is 5.
    2. Use the package resource to install git (as that is the RHEL package name).
  4. If the host is Windows, it will install git via the windows_package resource and instruct it to download the file located at node[:git][:url] (which in turn pulls from the default attributes or overridden configuration), validate that the checksum matches the one specified by node[:git][:checksum], and then install it; however, this is only if the EXE is not already installed.
  5. 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.
  6. 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.