Chef in 21 Days. Part II: Creating Your First Cookbook.

by Ievgen Kabanets

Greetings, readers! Are you still with us? Enjoying Chef so far? Pretty exciting, isn't it? Let us continue the voyage to achieving automation mastery that we have begun in Part I of this article. In Part II, we will talk about the first experience in writing a cookbook, as well as about recipes, attributes and templates.

Before we continue, I'd like to digress briefly into infrastructure. In the future, we're going to need a couple computers or virtual machines. There are VirtualBox and Vagrant, of course, but I strongly suggest using Amazon Web Services (AWS). The main reason is that many different companies use AWS, and learning to use it will definitely be a bonus. There are free images, available to anyone. Amazon offers a wide variety of tools and services. I consider some of them essential, and the overall experience is a must-have.

Step 5. Welcome to the kitchen

Welcome to the kitchen! We've got our most important and irreplaceable tool - knife!

So, what are we going to cook? Our very first recipe, of course, and in order to save it for later use, we will add it to our cookbook. That's where knife comes in, because it can create cookbooks. Of course, it is possible to create the structure of a cookbook manually, but why bother? I would much rather introduce you to the correct structure of a standard cookbook.

We're going to need the help of the knife cookbook create command. It will create a folder with our first cookbook in the /cookbooks directory (you can define its location in the knife.rb configuration file).

Let's take a look at the main contents of this folder.

  • /attributes

The /attributes directory contains files with cookbook attributes descriptions; these attributes can be used to overwrite the default values of the node. These can be variables containing paths to the work directories, network ports numbers for services, some node-specific tags (for example, the node role in the environment). The main thing about using the attributes is to understand how to apply them by means of a recipe.

So, how does Chef obtain the values of node-specific attributes? It has a tool called ohai that collects system attributes used by Chef. These attributes are not system variables, you are unlikely to see attribute names like that in your operating system; these variables have clear names, for example, the node["platform"] variable stores the name of the node OS, and node["fqdn"] stores the full domain name of the node. If you want to apply changes to the current configuration of a service/application, you must use the attribute names provided by ohai. Another important attribute feature is node identification - the assignment of tags (discussed in Part I). Here's an example from my experience.

I needed to create a group of virtual machines (chef-clients), connect them to the Chef server and send cookbooks to them according to their roles within the group (master or slave). During node registration, the Chef-server does not know which node is a master and which is a slave. What could I do to avoid doing this manually? The solution was incredibly simple. Each node, depending on the assignment, received an env_role = master or slave attribute accessible by the Chef server. The server, in its turn, searched all registered nodes using this attribute and, depending on the value, handed out the cookbooks (by use of roles, which we will discuss further).

  • /files

The /files directory contains any files meant to be distributed among the nodes. In other words, if you need to generate a file with static content on a node or deploy a self-contained executable or installer, this is the directory where this file is put to. Later, in a recipe, the cookbook_file resource initiates the usage of your file. Keep in mind, that the name of this resource specifies the absolute path to the file location on the node, so be attentive when defining the name. The source file is specified in the source resource property, where you give the name of the source file located in the /files directory. You can also set up access permissions to files on the nodes by specifying the users and user groups that "own" the given files.

The filename has another interesting feature. There are special filename templates that allow sending a specific file to a specific node (using FQDN) or using it on a specific platform with a specific current version. For example, a file with the filename ubuntu-12.04 will be handed out only to nodes with the Ubuntu operating system, version 12.04.

  • /recipes

The /recipes directory is the fundamental one for our cookbook, as it contains recipes. This is where it's decided what and how we will configure. The recipe itself is stored in a Ruby file. Depending on the complexity of the task, it can contain functionality descriptions and resource collections (for simple tasks), or resource collections and provider call requests which will be discussed later on. In a recipe, you can call other recipes, create dependencies on other recipes, insert script blocks for destination nodes (for example, bash or powershell commands), and employ other capabilities of the Ruby language. Keep in mind that the code is executed exactly in the order defined in the recipe. As a result, we can do a whole bunch of stuff, limited only by the facilities of Ruby and our knowledge of this language.

One thing to keep in mind, Chef is meant to be idempotent. When using any Chef providers to perform tasks, they handle this. If embedding a script block, you must take care that the script is only executed in an idempotent manner.

  • /templates

The /templates directory contains the templates of resources that can be used in recipes. Templates are defined in the Embedded Ruby format, have the .erb extension and describe the dynamic content based on the variables, logic or plain text. The template must be declared in a resource. The minimum declaration looks as follows:

template >/path/to/file/on/our/node.cmd> do source >node.erb> end

You can also define, for example, access permissions, an owner or a group of owners for this file. In this specific case, a cmd file is created on the node, containing an absolute path, the template of which is the node.erb file. This template can include a set of commands that must be run on the node from the command prompt. It can also contain variables and logic. The variables can pass cookbook attributes values, making the template dynamic, as discussed above. It may look the following way:

<%= node['sql_server']['product_key'] %>

It is the declaration of an attribute (SQL server key) existing in the cookbook. It should also be noted that, just like the filenames in the /files directory, template filenames also define the template's correspondence to a certain OS and version, or an FQDN node.

  • /providers, /resources

The /providers and /resources directories define LWRP (light-weight resource providers) in the Chef terminology. I would compare it with the notion of a class in object-oriented programming, but the comparison might not be entirely adequate. You can describe the functionality in the provider section, and the resources will contain the descriptions of attributes/variables and actions that are available to the providers and can be called from recipes. The resources are identified and associated with the provider, which bears similarity to a class.

Interactions between parts of LWRP are established by means of Resource DSL - a language that contains several methods for describing attributes and actions. And, considering that it's a Ruby DSL variation, Ruby code can also be a part of resource and provider definitions. Not to be too theoretical, here's an example of an LWRP that sends out notifications (for example, when a certain event takes place in the system).

/providers/default.rb
def mailing
puts "e;\n Sending your email..."e;
r = chef_gem "e;mail"e; do
action :install
end

Gem.clear_paths

require 'mail'
options = {
:address => new_resource.server,
:port => new_resource.port,
:mailto => new_resource.mailto,
:user_name => new_resource.user_name,
:password => new_resource.password,
:authentication => 'plain',
:enable_starttls_auto => true
}

Mail.defaults do
delivery_method :smtp, options
end

mail = Mail.deliver do
from options[:user_name]
to options[:mailto]
subject 'Hello'
body new_resource.msg
end

end

/resources/default.rb
actions :create
default_action :create

attribute :app_name, :kind_of => String
attribute :user_name, :kind_of => String, :default => "e;"e;
attribute :password, :kind_of => String, :default => "e;"e;
attribute :server, :kind_of => String, :default => "e;"e;v attribute :port, :kind_of => Integer, :default => 465
attribute :mailto, :kind_of => [String, NilClass], :default => "e;"e;
attribute :from, :kind_of => [String, NilClass], :default => "e;"e;
attribute :msg, :kind_of => String, :default => "e;Hi there"e;
def initialize(*args)
super
@ action = :create
End

I hope this description will be fairly obvious even to those who've never dealt with Ruby syntax before. It's an email sending function. In Part III we will take a look at a cookbook that uses LWRP to send messages about system changes.

Time to have a taste

So, let's imagine that our cookbook is complete (I will provide an example and describe its usage in the next article).

What do we do next? How do we upload the cookbook to the server and send it to the nodes for execution? Knife will do that for us. In order to perform further actions successfully, we need the Chef-admin to have a correctly configured knife tool (knife.rb), and an administrator's certificate (for example, admin.pem). Then we need to go to the /chef-repo directory (the same place where our starter kit is located, for example) and run commands from it.

The first thing we need to do is to make sure that the nodes are registered on the server - see the knife node list. We should be able to get the names of all active nodes.

Now let's upload our cookbook to the server. It must be located in the directory defined in the cookbook_path property of the knife.rb file.

Run knife cookbook upload name-of-cookbook. This should result in getting a message about a successful upload and seeing our cookbook in the list obtained by the knife cookbook list command.

The Next step is to define the run-list node. There are two ways to do it: the simple one and the right one.

The simple way: add the recipe straight to the run-list - knife node edit name-of-node, and edit the run-list section, adding a line "recipe[name-of-recipe]". But what if there's a dozen recipes? This becomes inconvenient.

The right way: add a role. A role file is a description of run-list attributes, including recipes. Therefore one role can be assigned to several nodes. The role file is located at /chef-repo/roles:

name "mailer" description "Role for host that will notify us on changes" run_list "recipe[name-of-recipe]"

To upload a role to the server, use the knife role from file name-of-role-file command. After the upload, the role can be defined in a node's run-list, which is what we're going to do.

Thus, the simple setup process is complete and we are ready to launch the client on the node. After that we should have a node with a (relatively) implemented cookbook.

This is the end of Part II, and this is where I will announce Part III - it will contain more technical details and more code. We'll also see AWS and Chef in action.