Balaji Vajjala's Blog

A DevOps Blog from Trenches

CFEngine Puppet and Chef Part 3

At the end of the last installment, we used Puppet to create a Chef server. That brings us full circle, and the only thing we have left to do is examine how Chef works. We’ll do that by looking at the code that gave us our original CFEngine server.

Chef

Since they’re both written in Ruby, people tend to compare Puppet and Chef. This is natural since they have a lot in common. Both are convergence based configuration management tools inspired by CFEngine. Both have stand alone discovery agents (facter and ohai, respectively), as well as RESTful APIs for gleaning node information from the server. It turns out, however, that Chef actually has a lot more in common with CFEngine.

Like CFEngine, Chef copies policy from the server and evaluates it on the edges. This allows for high scalability, since the server isn’t doing very much. Think of web application that does most of its work in the browser instead of on the server.

A Chef recipe is a collection of convergent resource statements, and serves as the basic unit of intent. This is analogous to a CFEngine promise bundle. The Chef run list is how recipe ordering is defined, and is directly comparible to CFEngine’s bundlesqeuence. Using this approach makes it easy to reason about what’s going on when writing infrastructure as code.

Chef Specials

Imperative programming and declarative interface

While it’s true that Chef is just “pure ruby” and therefore imperative, to say that Chef is imperative without considering the declarative interface to resources is disingenuous at best. Using nothing but Chef resources, recipes look very much like their CFEngine and Puppet counterparts. The non-optimally ordered Chef version of NTP converges in the same number of runs as the CFEngine example from the first installment. This is because the underlying science of convergent operators is the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# service
service "ntp" do
  action [ :enable, :start ]
  ignore_failure true
end

# file 
template "/etc/ntp.conf" do
  source "ntp.conf.erb"
  owner "root"
  group "root"
  mode 0644
  notifies :restart, "service[ntp]"
  ignore_failure true
end

# package 
package "ntp" do
  action :install
  ignore_failure true
end

When and where order matters, imperative ordering isolated within a recipe is the most intuitive way for sysadmins to accomplish tasks within the convergent model. “Install a package, edit a config file, and start the service” is how most people think about the task. Imperative ordering of declarative statements give the best of both worlds. When order does NOT matter, it’s safe to re-arrange recipe ordering in the Chef run list.

Multiphase execution

The real trick to effective Chef cookbook development is to understand the Anatomy of a Chef Run. When a Chef recipe is evaluated in the compilation phase, encountered resources are added to the Resource Collection, which is an array of evaluated resources with deferred execution.

The compile phase of this recipe would add 99 uniquely named, 12 oz, convergent beer_bottles to the collection, and the configure phase would take them down and pass them around. Subsequent runs would do nothing.

thanks jtimberman!
1
2
3
4
5
6
7
8
size = ((2 * 3) * 4) / 2

99.downto(1) do |i|
  beer_bottle "bottle-#{i}" do
    oz size
    action [ :take_down, :pass_around ]
  end
end

The idea is that you can take advantage of the full power of Ruby to make decisions about what to declare about your resources. Most people just use the built in Chef APIs to consult chef-server for topology information about their infrastructure. However, there’s nothing stopping you from importing random Ruby modules and accessing existing SQL databases instead.

Want to name name servers after your Facebook friends? Go for it. Want your MOTD to list all James Brown albums released between 1980 and 1990? Not a problem. The important part is that things are ultimately managed with a declarative, idempotent, and convergent resource interface.

cfengine.rb

Let’s take a look at the recipe that gave us our original CFEngine server.

File /Users/balajivajjala/Development/opress-src/bvajjala.github.io/source/downloads/code/cookbooks/cfengine/recipes/server.rb could not be found

Topology management

When a node is bootstrapped with Chef, a run list of roles or recipes is requested by the node itself. After that, the host is found by recipes running elsewhere in the infrastructure by searching for roles or attributes. This is contrasted from the CFEngine and Puppet techniques of matching classes based on a hostname, FQDN, IP, or other found information.

This approach has the effect of decoupling a node’s name from its functionality. Line 10 in cfengine.rb above searches out node objects and later be passes them to the promises-server.cf.erb template for authorization.

Wrapping up

So there you have it folks. Chef making CFEngine making Puppet making Chef. These tools can be used to automate literally anything, and they’re pretty easy to use once you figure out how they work. I was going to throw some Bcfg2 and LCFG in there just for fun, but I only had some much free time =)

Configuration mangement is like a portal.

-s