Balaji Vajjala's Blog

A DevOps Blog from Trenches

Writing a Chef Cookbook, or writing your first cookbook

In continuation to a Chef Introduction session we had last week on meetup, I thought a blog post was called for in order to emphasize the process of writing a recipe. And/Or working with chef in general as a buy product of that.

I will be using the basic “ntp” example Opscode uses on their wiki, but in order to understand the components of a recipe I will stretch it a bit further in order to show the true power of Attributes and Templates.

At the end of this post [or now if you really insist] you can clone https://github.com/tikalk/chef-intro-repo and see the ntp recipe alongside other stuff which was presented in the meetup.

Prequisets:

  1. A Chef server [see: http://www.tikalk.com/alm/blog/installing-chef-server if you want and set one up …]
  2. Configured Knife workstation

Step 1: Install the service you want to recipe …

1
2
    yum install ntp
    rpm -ql ntp

Identify:

  • package name you wish to install
  • Files which are template candidates [which chef will need to populate with your data]

I found the following:

  • service name “ntpd” [ init file name: /etc/rc.d/init.d/ntpd ]
  • configuration file our tempalte candidate “/etc/ntp.conf” [ also found via rpm -ql ]

Step 2: Setup a git repository [clone opscode’s “template” repository]

1
    git clone git://github.com/opscode/chef-repo.git

Step 3: Create a cookbook [named ntp]

1
2
    cd ~/chef-repo
    knife cookbook create ntp

The knife command above will create the foloowing structure [under cookbooks directory]:

1
2
3
4
5
6
7
8
9
10
    attributes/
    definitions/
    files/
    libraries/
    metadata.rb
    providers/
    README.rdoc
    recipes/
    resources/
    templates/

We won’t be using all of these in this tutorial … highlighted are the ones we are going to use (at this stage)

Step 4: Create the recipe

1
    vim  cookbooks/ntp/recipes/default.rb

Add the following ruby code [link]:

1
2
3
4
5
6
7
8
9
10
11
12
13
package "ntp" do
    action [:install]
end

template "/etc/ntp.conf" do
    source "ntp.conf.erb"
    variables( :ntp_server => "time.nist.gov" )
    notifies :restart, "service[ntpd]"
end

service "ntpd" do
    action [:enable,:start]
end

The first block of code will use chefs built-in packadge installer providor to use the os’s package manager (in our case yum/rpm) and use the service name “ntp” the one we located whilst installing the package in step1 above.

Step 5: Create a template

1
vim cookbooks/ntp/templates/default/ntp.conf.erb

Unlike files which are placed by chef “as is” files under templates folder ending with erb are interpolated and created on the node during a chef-client run.

The content of the file [link]:

1
2
3
4
5
6
7
8
restrict default kod nomodify notrap nopeer noquery
restrict -6 default kod nomodify notrap nopeer noquery
restrict 127.0.0.1
restrict -6 ::1
server <%= @ntp_server %>
server  127.127.1.0     # local clock
    driftfile /var/lib/ntp/drift
    keys /etc/ntp/keys

In this simple use case line #7 from cookbooks/ntp/recipes/default.rb will be the one setting the ntp_server parameter for the tempalte file in line #5 of the template above.

At this stage you could create a role add this recipe to arun_list and it will just work … until you try to apply this recipe on ubuntu for example, there you will find an issue with the service name … and whilst were at it , let’s add support for more than one ntp server [in case the single one we added is down :(].

1
    apt-get install ntp

Will reveal the issue I just mentioned and

1
2

dpkg -L ntp

will give us the list of files [ /etc/ntp.conf ] and service name [ ntp ] => notice in this case there is no “d” at the end.

Step 6: Improovment #1 – adding service name resolution to our recipe

Add an attributes file:

1
    vim  cookbooks/ntp/attributes/default.rb

With the following content:

1
2
3
4
5
6
7
8
    case platform
    when "redhat","centos","fedora","scientific"
      default[:ntp][:service] = "ntpd"
    when "ubuntu","debian"
      default[:ntp][:service] = "ntp"
    else
      default[:ntp][:service] = "ntpd"
    end

This case statement will help our recipe in the service name resolution for redhat / centos & other rpm based distros “ntpd” for ubutnu / debian use “ntp”.

Let’s tell our recipe to respect this attribute …:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    package "ntp" do
        action [:install]
    end

    service node[:ntp][:service] do
        service_name node[:ntp][:service]
        action [:enable,:start,:restart]
    end

    template "/etc/ntp.conf" do
        source "ntp.conf.erb"
        owner "root"
        group "root"
        mode 0644
        notifies :restart, resources(:service => node[:ntp][:service])
    end

The diff is in line #7 & line #12 which now uses the node:[ntp][:service] attribute we defined in the attributes.rb file above.

Step 7: Add support for more than 1 ntp server

In cookbooks/ntp/attributes/default.rb file add the following array:

1
    default[:ntp][:servers] = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"]

And in our template file let’s add support for more than one line of ntp server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    # Generated by Chef for <%= node[:fqdn] %> 
    # node[:fqdn] = ohai data collected on node !
    # Local modifications will be overwritten.

    restrict -6 ::1
    #server <%= @ntp_server %>

    <% node[:ntp][:servers].each do |ntpsrv| -%>
      server <%= ntpsrv %> iburst
      restrict <%= ntpsrv %> nomodify notrap noquery
    <% end -%>

    restrict default kod nomodify notrap nopeer noquery
    restrict -6 default kod nomodify notrap nopeer noquery
    restrict 127.0.0.1

    server  127.127.1.0     # local clock
    driftfile /var/lib/ntp/drift
    keys /etc/ntp/keys

As you can see I marked out linet #5 which is our old ntp decleration

and added lines# 7-10:

1
2
3
4
    <% node[:ntp][:servers].each do |ntpsrv| -%>
      server <%= ntpsrv %> iburst
      restrict <%= ntpsrv %> nomodify notrap noquery
    <% end -%>

chef will itterate over the array and inject the vlaues in our case whilst using defaults we will recieve the following:

1
2
3
4
5
6
7
8
      server 0.pool.ntp.org iburst
     restrict 0.pool.ntp.org nomodify notrap noquery
      server 1.pool.ntp.org iburst
     restrict 1.pool.ntp.org nomodify notrap noquery
      server 2.pool.ntp.org iburst
     restrict 2.pool.ntp.org nomodify notrap noquery
      server 3.pool.ntp.org iburst
     restrict 3.pool.ntp.org nomodify notrap noquery

That’s it all is left is to upload this cookbook to your chef server and add it to one of your nodes and you are good to go.

1
    knife cookbook upload ntp

(you might want to bump the version up just so it becomes a habbit – edit the metadata.rb file under the recipies directory).

Coming up [hopefully in the next few days] a test environment for chef cookbooks – I will be taking this example and present how to setup an environment for testing combining Chef-Solo (in case you don’t have a chef server), Vergrant & VirtualBox

Fell free to comment / remark :)