Cooking Infrastructure by Chef

Alexey Vasiliev aka leopard and Contributors

Creative Commons Attribution-Noncommercial 4.0 International
2014

Introduction

Chef is a configuration management and automation platform from Chef. Chef helps you describe your infrastructure with code. Because your infrastructure is managed with code, it can be automated, tested and reproduced with ease.

So, what is Chef?

[fig:cheflogo]

Chef is a configuration management tool written in Ruby and Erlang. It uses a pure-Ruby, domain-specific language (DSL) for writing system configuration «recipes». Chef is used to streamline the task of configuring and maintaining a company’s servers, and can integrate with cloud-based platforms such as Rackspace and Amazon EC2 to automatically provision and configure new machines.

The user writes «recipes» that describe how Chef manages server applications (such as Apache, MySQL, or Hadoop) and how they are to be configured. These recipes describe a series of resources that should be in a particular state: packages that should be installed, services that should be running, or files that should be written. Chef makes sure each resource is properly configured and corrects any resources that are not in the desired state.

Traditionally, Chef is used to manage GNU/Linux but later versions support running on Windows as well.

What are the core principles?

Idempotence

A recipe can run multiple times on the same system and the results will always be identical. A resource is defined in a recipe, which then defines the actions to be performed on the system. The chef-client ensures that actions are not performed if the resources have not changed and that any action that is performed is done the same way each time. If a recipe is re-run and nothing has changed, then the chef-client will not do anything.

Thick Clients, Thin Server

Chef does as much work as possible on the node and as little as possible on the server. A chef-client runs on each node and it only interacts with the server when it needs to. The server is designed to distribute the data to each node easily, including all cookbooks, recipes, templates, files, and so on. The server also retains a copy of the state of the node at the conclusion of every chef-client run. This approach ensures that the actual work needed to configure each node in your infrastructure is distributed across the organization, rather than centralized on smaller set of configuration management servers.

Order Matters

When the chef-client configures each node in the system, the order in which that configuration occurs is very important. For example, if Apache is not installed, then it cannot be configured and the daemon cannot be started. Configuration management tools have struggled with this problem for a long time. For each node a list of recipes is applied. Within a recipe, resources are applied in the order in which they are listed. At any point in a recipe other recipes may be included, which assures that all resources are applied. The chef-client will never apply the same recipe twice. Dependencies are only applied at the recipe level (and never the resource level). This means that dependencies can be tracked between high-level concepts like «I need to install Apache before I can start my Django application!» It also means that given the same set of cookbooks, the chef-client will always execute resources in the same exact order.

Why you should use Chef?

There are several reasons for using Chef:

And of course the main point is shown on picture [fig:automate-all-the-things].

[fig:automate-all-the-things]

What doesn’t Chef do?

Summary

The key underlying principle of Chef is that you (the user) knows best about what your environment is, what it should do, and how it should be maintained. The chef-client is designed to not make assumptions about any of those things. Only the individuals on the ground — that’s you and your team—understand the technical problems and what is required to solve them. Only your team can understand the human problems (skill levels, audit trails, and other internal issues) that are unique to your organization and whether any single technical solution is viable.

The idea that you know best about what should happen in your organization goes hand-in-hand with the notion that you still need help keeping it all running. It is rare that a single individual knows everything about a very complex problem, let alone knows all of the steps that may be required to solve them. The same is true with tools. Chef provides help with infrastructure management, and can help solve very complicated problems. Chef also has a large community of users who have a lot of experience solving a lot of very complex problems. That community can provide knowledge and support in areas that your organization may not have and (along with Chef) can help your organization solve any complex problem.

Chef Solo

Chef Solo is simple way to begin working with Chef. It is an open source version of the chef-client that allows using cookbooks with nodes without requiring access to a server. Chef Solo runs locally and requires that a cookbook (and any of its dependencies) be on the same physical disk as the node. It is a limited-functionality version of the chef-client and does not support the following:

We will learn Chef Solo by practical examples in this chapter.

Required software

To get started working with Chef, you need to install the required software:

To get started with Chef, we need to create chef-repo (kitchen), which will contain all the data for a chef client(s). For example, I’ll create a directory «my-cloud», which will contain a chef-repo (kitchen). In this directory I create a file «Gemfile» with such content:

source "https://rubygems.org"

gem 'knife-solo'
gem 'berkshelf'

and run command bundle in terminal. As a result, by using bundler, we install any required rubygems.

Rubygems and bundler

RubyGems is a package manager for the Ruby programming language that provides a standard format for distributing Ruby programs and libraries (in a self-contained format called a «gem»), a tool designed to easily manage the installation of gems, and a server for distributing them. It is analogous to EasyInstall for the Python programming language.

Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed. Bundler is an exit from dependency hell, and ensures that the gems you need are present in development, staging, and production.

But why do we need these rubygems?

Knife-solo

Knife is a command-line tool that provides an interface between a local chef-repo and the Chef server. But for Chef Solo is better to install the knife-solo plugin. It will install knife as dependency and adds 5 subcommands to knife tool:

Knife-solo also integrates with Berkshelf and Librarian-Chef.

Berkshelf

Berkshelf is used as well as the bundler for rubygems - it manages cookbooks and their dependencies. Also, there is librarian-chef, which performs a similar function. I prefer to use berkshelf, because it has more features and integrations.

Creation of kitchen (chef-repo)

Working with Chef Solo starts with creating kitchen (chef-repo). The kitchen is located on a workstation (the location from which most users will do most of their work, ie. your computer) and should be synchronized with a version control system. To create a kitchen use knife-solo rubygems:

$ cd my-cloud
$ knife solo init .
WARNING: No knife configuration file found
Creating kitchen...
Creating knife.rb in kitchen...
Creating cupboards...
Setting up Berkshelf...
$ ls -o
total 32
-rw-r--r--  1 leo    14 Dec 14 00:36 Berksfile
-rw-r--r--@ 1 leo    63 Dec 14 00:36 Gemfile
-rw-r--r--  1 leo  4427 Dec 14 00:36 Gemfile.lock
drwxr-xr-x  3 leo   102 Dec 14 00:36 cookbooks
drwxr-xr-x  3 leo   102 Dec 14 00:36 data_bags
drwxr-xr-x  3 leo   102 Dec 14 00:36 environments
drwxr-xr-x  3 leo   102 Dec 14 00:36 nodes
drwxr-xr-x  3 leo   102 Dec 14 00:36 roles
drwxr-xr-x  3 leo   102 Dec 14 00:36 site-cookbooks

Let’s consider the directory structure:

«Cookbooks» directory added into «.gitignore», because it contains only vendor cookbooks. Vendor cookbook data will not be modified, so there is no reason to keep them in VCS (git, mercurial, etc).

.Chef folder

The «.chef» directory that is used to store .pem files and the knife.rb file.

For Chef Solo in this directory mostly contained only knife.rb file. A knife.rb file is used to specify the chef-repo-specific configuration details for Knife. This file is the default configuration file and is loaded every time this executable is run. The configuration file is located at: ~/.chef/knife.rb. If a knife.rb file is present in the .chef/knife.rb directory in the chef-repo, the settings contained within that file will override the default configuration settings.

In example, knife.rb have such content:

cookbook_path    ["cookbooks", "site-cookbooks"]
node_path        "nodes"
role_path        "roles"
environment_path "environments"
data_bag_path    "data_bags"
#encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"

Let’s look at the meaning of these options:

Vendor cookbooks and berkshelf

Suppose, that our task to install Apache2 on node. For this purpose we can use vendor cookbook through berkshelf. Huge amount cookbooks you can find at Chef community website. To install Apache2 cookbook we need add it to Berksfile:

source "http://api.berkshelf.com"

cookbook 'apache2'

After running the command berks install this cookbook will be installed with dependencies.

$ berks install
Using apache2 (1.7.0)

By default, you will not find this cookbook in «cookbooks» directory. Berkshelf install cookbooks in « /.berkshelf» directory (to avoid duplication of cookbooks, if you have several chef-repo). To install it in special path you can use vendor command:

$ berks vendor cookbooks
Using apache2 (1.7.0)
$ ls cookbooks
apache2

Option knife[:berkshelf_path] in knife.rb file set to install our cookbooks into cookbooks directory. In this case no need to run berks install --path cookbooks each time, when you need «cook» a node - knife-solo will do it automatically.

Now we can start to work with a nodes.

Defining nodes

Node itself represent any physical, virtual, or cloud machine. In most cases number of nodes in kitchen equal of number of machines in your cloud. To install apache2 to the correct server, we need to create a file in the nodes folder. Basically, this file is named as machine domain. For example, I attach to the machine domain «web1.example.com» and create node for it:

{
  "run_list": []
}

Node file should contain valid JSON document. Main key in this json is run_list. This key contain array of recipes and roles, which should be executed on machine. It always executed in the same order as listed in this key. As you can see right now run_list is empty. To install apache2 you need to add the recipe in this array. This information can be found in README of cookbook (if this vendor cookbook well written), in metadata.rb or just open directory recipes - the name of the file will mean recipe name. default.rb file mean a recipe that will be executed when you call the cookbook in run_list without designation of recipe. Examples:

{
  "run_list": [
    "recipe[apache2]"
  ]
}

This run_list will execute default recipe from apache2 cookbook.

Now we are ready to test our kitchen.

Vagrant

For testing our chef kitchen in most cases we are using Vagrant. So what is Vagrant?

Vagrant is free and open-source software for creating and configuring virtual development environments. It can be considered a wrapper around VirtualBox and configuration management software such as Chef. Since version 1.1, Vagrant is no longer tied to VirtualBox and also works with other virtualization software such as VMware and Amazon EC2.

Instead of building a virtual machine from scratch, which would be a slow and tedious process, Vagrant uses a base image to quickly clone a virtual machine. These base images are known as boxes in Vagrant, and specifying the box to use for your Vagrant environment is always the first step after creating a new Vagrantfile. For testing will be used Ubuntu 12.04 LTS 64-bit (precise64 box).

$ vagrant box add precise64 http://files.vagrantup.com/precise64.box

More boxes you can find on Vagrantbox.es or Vagrant Cloud.

The first step for any project to use Vagrant is to configure Vagrant using a Vagrantfile. We should execute vagrant init precise64 inside kitchen directory:

$ vagrant init precise64
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

By default, Vagrantfile have such content:

# -*- mode: ruby -*-
# vi: set ft=ruby :

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise64"
end

We can check, what vagrant is working fine by command «vagrant up»:

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'precise64'...
[default] Matching MAC address for NAT networking...
[default] Setting the name of the VM...
[default] Clearing any previously set forwarded ports...
[default] Creating shared folders metadata...
[default] Clearing any previously set network interfaces...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)
[default] Booting VM...
[default] Waiting for machine to boot. This may take a few minutes...
[default] Machine booted and ready!
[default] Mounting shared folders...
[default] -- /vagrant

After this you can use command vagrant ssh to SSH into a running Vagrant machine and give you access to a shell. Command «vagrant halt» shuts down the running machine Vagrant is managing. vagrant destroy command stops the running machine Vagrant is managing and destroys all resources that were created during the machine creation process.

In most cases, your machine will not contain chef client. We should install it before using our kitchen. As you remember, we have knife with command prepare. Let use this command:

$ knife solo prepare vagrant@localhost -i ~/.vagrant.d/insecure_private_key -p 2222 -N web1.example.com
Bootstrapping Chef...
--2013-12-27 19:12:56--  https://www.opscode.com/chef/install.sh
Resolving www.opscode.com (www.opscode.com)... 184.106.28.91
...
Installing Chef 11.8.2
installing with dpkg...
Selecting previously unselected package chef.
(Reading database ... 51095 files and directories currently installed.)
Unpacking chef (from .../chef_11.8.2_amd64.deb) ...
Setting up chef (11.8.2-1.ubuntu.12.04) ...
Thank you for installing Chef!

We used some options for the knife prepare command. -i option specify ssh key for machine, -N option specify node name (if it different from host name) and -p option specify SSH port. In most cases, this port is 2222, but if you running several machines from vagrant, it will be different. You can read what port is used for SSH by machine from output of command vagrant up. All this credentials used to login by ssh on the node and install chef client.

Basically, to run our kitchen on node we are using knife solo cook command:

$ knife solo cook vagrant@localhost -i ~/.vagrant.d/insecure_private_key -p 2222 -N web1.example.com
Running Chef on localhost...
Checking Chef version...
Installing Berkshelf cookbooks to 'cookbooks'...
Using apache2 (1.7.0)
Uploading the kitchen...
...
 * service[apache2] action start (up to date)
Chef Client finished, 1 resources updated

But in Vagrant we can use build in command vagrant provision. This command runs any configured provisioners against the running Vagrant managed machine.

Vagrantfile inside use ruby syntax, so we can use Ruby to DRY our config. We should install chef gem inside vagrant:

$ vagrant plugin install chef
Installing the 'chef' plugin. This can take a few minutes...
Installed the plugin 'chef (11.8.2)'!

After this we can use chef gem inside Vagrantfile config:

# -*- mode: ruby -*-
# vi: set ft=ruby :

require 'chef'
require 'json'

Chef::Config.from_file(File.join(File.dirname(__FILE__), '.chef', 'knife.rb'))
vagrant_json = JSON.parse(Pathname(__FILE__).dirname.join('nodes', (ENV['NODE'] || 'web1.example.com.json')).read)

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise64"

  config.vm.provision :chef_solo do |chef|
    chef.cookbooks_path = Chef::Config[:cookbook_path]
    chef.roles_path = Chef::Config[:role_path]
    chef.data_bags_path = Chef::Config[:data_bag_path]

    chef.environments_path = Chef::Config[:environment_path]
    #chef.environment = ENV['ENVIRONMENT'] || 'development'

    chef.run_list = vagrant_json.delete('run_list')
    chef.json = vagrant_json
  end
end

Now consider that we have added.

On lines 4-5 required chef and json gem. JSON gem is as part of Vagrant and chef gem was installed by previous command vagrant plugin install chef. Further, we load knife.rb file in chef config and parse json from node file «web1.example.com.json». After these manipulations we will have Chef::Config ruby hash with knife configuration and vagrant_json ruby hash with attributes from node.

On lines 16-26 defined Chef Solo configuration for Vagrant (environment is commented, because we don’t have it right now, but we will use it later). More information about this you can find in vagrant website.

Because we changed the Vagrantfile configuration, we need to restart the test node by using vagrant reload command:

$ vagrant reload
[default] Attempting graceful shutdown of VM...
...
[default] -- /vagrant
[default] -- /tmp/vagrant-chef-1/chef-solo-3/roles
[default] -- /tmp/vagrant-chef-1/chef-solo-2/cookbooks
[default] -- /tmp/vagrant-chef-1/chef-solo-1/cookbooks
[default] -- /tmp/vagrant-chef-1/chef-solo-4/data_bags
[default] -- /tmp/vagrant-chef-1/chef-solo-5/environments
$ vagrant provision
[default] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
stdin: is not a tty
INFO: Forking chef instance to converge...
INFO: *** Chef 11.8.2 ***
...
INFO: service[apache2] restarted
INFO: Chef Run complete in 18.115479422 seconds
INFO: Running report handlers
INFO: Report handlers complete

To verify that the apache2 successfully installed into node, we can forward the 80 apache2 port to our machine. To do this, we modify the Vagrantfile:

...

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "precise64"
  config.vm.network :forwarded_port, guest: 80, host: 8085 # <== add port forwarding

...

And again reload node:

$ vagrant reload
...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)
[default] -- 80 => 8085 (adapter 1)
...

Now in any of your browser you can open url http://localhost:8085/ and see 404 page from apache2 server (because we only install it).

Idempotence

As you read from previous chapter, one of the main idea of Chef is idempotence. It mean, what Chef can safely be run multiple times on the same machine. Once you develop your configuration, your machines will apply the configuration and Chef will only make any changes to the system if the system state does not match the configured state.

For now we have machine which contain apache2 running inside it. Let’s run vagrant provision again:

$ vagrant provision
[default] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
stdin: is not a tty
...
INFO: Chef Run complete in 1.092279021 seconds
...

As you can see, chef client did nothing, because configuration of the server the same as in chef kitchen (what is why execution time also so small).

Defining roles

Roles help classify the same server group. For example, in your project you can have web, queue and db servers. In this case you can create such type of roles, which will include the same attributes and run_list for nodes. Let’s look at an example.

For example, in our project we have a web application servers, load balancer server and database server. First we will create roles «web»:

{
  "name": "web",
  "description": "The base role for systems that serve web server",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "default_attributes": {
    "apache": {
      "listen_ports": ["80", "443"]
    }
  },
  "run_list": [
    "recipe[apache2]"
  ]
}

Let’s consider current json structure:

To use this role, we can create new node «web2.example.com» with content:

{
  "run_list": [
    "role[web]"
  ]
}

And check that everything is working:

$ NODE=web2.example.com.json vagrant provision
[default] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
stdin: is not a tty
INFO: Forking chef instance to converge...
INFO: *** Chef 11.8.2 ***
INFO: Chef-client pid: 1224
INFO: Setting the run_list to ["role[web]"] from JSON
INFO: Run List is [role[web]]
INFO: Run List expands to [apache2]
...
INFO: Chef Run complete in 1.437157496 seconds
INFO: Running report handlers
INFO: Report handlers complete

As you can see, role defined in run_list by command role and role command replaced to run list of this role by chef client. This allow for use use several roles with recipes in the same node. For example, if web and database role should exists on the same node (example: staging environment), you can define run_list in node in such way:

{
  "run_list": [
    "role[web]",
    "role[db]"
  ]
}

BTW, role can contain in run list another roles. For example:

{
  "name": "test",
  "description": "The test role, it is not used in kitchen",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "run_list": [
    "role[web]",
    "recipe[postgresql]"
  ]
}

Role can contain attributes inside default_attributes or override_attributes keys. In our example, «web» role can contain general settings for http listen ports, timeout of web server, etc. So let’s look in more detail about the use of attributes.

Attributes

An attribute is a specific detail about a node. Attributes are used by the chef-client to understand:

In our example we install and configure by chef apache 2 web server. In vendor cookbook «apache2» exists default attributes.

# General settings
default['apache']['listen_ports'] = ["80"]
default['apache']['contact'] = "ops@example.com"
default['apache']['timeout'] = 300
default['apache']['keepalive'] = "On"
default['apache']['keepaliverequests'] = 100
default['apache']['keepalivetimeout'] = 5
...

This attributes used, because we don’t override its by environment, role or node. For example, we can add 443 port through our «web» role:

{
  "name": "web",
  "description": "The base role for systems that serve web server",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "default_attributes": {
    "apache": {
      "listen_ports": ["80", "443"]
    }
  },
  "run_list": [
    "recipe[apache2]"
  ]
}

Also we can set keepalivetimeout for apache in node attributes:

{
  "apache": {
    "keepalivetimeout": 30
  },
  "run_list": [
    "role[web]"
  ]
}

If we will set listen_ports in node attribute, when this will override role listen_ports attribute:

{
  "apache": {
    "keepalivetimeout": 30,
    "listen_ports": ["80", "443", "8080"]
  },
  "run_list": [
    "role[web]"
  ]
}

But role can override attributes, that should be applied to all nodes, even if the node already has a value for an attribute. This is useful for ensuring that certain attributes always have specific values. All this attributes should be written in override_attributes key (instead of default_attributes):

{
  "name": "web",
  "description": "The base role for systems that serve web server",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "override_attributes": {
    "apache": {
      "listen_ports": ["80", "443"]
    }
  },
  "run_list": [
    "recipe[apache2]"
  ]
}

More info about attributes you can find on wiki page.

Defining environments

An environment is a way to map an organization’s real-life workflow to what can be configured and managed when using server. Every organization begins with a single environment called the _default environment, which cannot be modified (or deleted). Additional environments can be created to reflect each organization’s patterns and workflow. For example, creating production, staging, testing, and development environments. Generally, an environment is also associated with one (or more) cookbook versions.

We create for our example development environment:

{
  "name": "development",
  "description": "development environment",
  "chef_type": "environment",
  "json_class": "Chef::Environment",
  "default_attributes": {
    "apache": {
      "timeout": 120
    }
  }
}

Let’s consider a json structure:

As you can see environment doesn’t have run_list, but it have attributes. Attributes in most cases contain information, which specific for environment: connection information to databases (hostname, port, etc.), cluster settings for database or queue, etc.

Now we can activate this development environment in Vagrantfile:

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ...
    chef.environments_path = Chef::Config[:environment_path]
    chef.environment = ENV['ENVIRONMENT'] || 'development'
  ...
end

and check how it works:

$ vagrant provision
...
[2013-12-31T21:53:57+00:00] INFO: Chef Run complete in 1.105324838 seconds
[2013-12-31T21:53:57+00:00] INFO: Running report handlers
[2013-12-31T21:53:57+00:00] INFO: Report handlers complete

To cook server by knife with environment you should use -E argument:

$ knife solo cook vagrant@localhost -i ~/.vagrant.d/insecure_private_key -p 2222 -N web1.example.com -E development
Running Chef on localhost...
Checking Chef version...
...
  * service[apache2] action start (up to date)
Chef Client finished, 1 resources updated

You can set environment for node by attribute environment:

{
  "environment": "development",
  "run_list": [
    "recipe[apache2]"
  ]
}

and use command knife solo cook without -E argument.

Once an environment exists on the server, a node can be associated with that environment using the chef_environment method of «node» object in Ruby (I’ll consider this in the next chapters).

As you can see in Chef Solo environment can be used only for setting attributes. In Chef Server it have additional feature for locking cookbook versions, which we will consider in «[sec:server-environment] » chapter.

Defining data bags

A data bag is a global variable that is stored as JSON data. The contents of a data bag can vary, but they often include sensitive information (such as database passwords).

Knife is not working with Chef Solo data bags, but we can use knife-solo_data_bag rubygem. Just add this gem in Gemfile:

source "https://rubygems.org"

gem 'knife-solo'
gem 'knife-solo_data_bag'
gem 'berkshelf'

and run command bundle to install it. After installation you knife should have new commands for working with Chef Solo:

$ knife --help | grep solo
  knife solo cook [USER@]HOSTNAME [JSON] (options)
  knife solo init DIRECTORY
  knife solo prepare [USER@]HOSTNAME [JSON] (options)
knife solo bootstrap [USER@]HOSTNAME [JSON] (options)
knife solo clean [USER@]HOSTNAME
knife solo cook [USER@]HOSTNAME [JSON] (options)
knife solo init DIRECTORY
knife solo prepare [USER@]HOSTNAME [JSON] (options)
knife solo data bag create BAG [ITEM] (options)
knife solo data bag edit BAG ITEM (options)
knife solo data bag list (options)
knife solo data bag show BAG [ITEM] (options)
  knife solo clean [USER@]HOSTNAME

For beginning, let’s create a plain text data bag:

$ EDITOR=vim knife solo data bag create pass mysql
Created data_bag_item[mysql]

EDITOR environment variable is used to set editor, which will open data bag file. I add in this JSON password for database and save it. Now we can see the result:

$ knife solo data bag show pass
mysql:
  id:       mysql
  password: secret

If you open data bag file, you will see this JSON:

{
  "name":"data_bag_item_pass_mysql",
  "chef_type":"data_bag_item",
  "json_class":"Chef::DataBagItem",
  "data_bag":"pass",
  "raw_data":{
    "id":"mysql",
    "password":"secret"
  }
}

Let’s consider a json structure:

The contents of a data bag can be encrypted using shared secret encryption. This allows a data bag to store confidential information (such as a database password) or to be managed in a source control system (without plain-text data appearing in revision history).

Encrypting a data bag requires a secret key. A secret key can be created in any number of ways. For example, OpenSSL can be used to generate a random number, which can then be used as the secret key:

$ openssl rand -base64 512 | tr -d '\r\n' > .chef/encrypted_data_bag_secret

The tr command eliminates any trailing line feeds. Doing so avoids key corruption when transferring the file between platforms with different line endings.

A data bag can be encrypted using a Knife command similar to:

$ EDITOR=vim knife solo data bag create passwords mysql --secret-file .chef/encrypted_data_bag_secret
Created data_bag_item[mysql]

Editor is opened with such content:

{
  "id": "mysql"
}

And you can add you password and save it:

{
  "id": "mysql",
  "password": "secret"
}

As a result we obtain the encrypted data bag:

$ knife solo data bag show passwords mysql
id:       mysql
password:
  cipher:         aes-256-cbc
  encrypted_data: ++RR0s5f3rypFO3+SZj25px9QHTFq7AN854F3XZotnQ=

  iv:             fxvNqHFHHbCNKntW8bBJkg==

  version:        1

An encrypted data bag item can be decrypted with a Knife command similar to:

$ knife solo data bag show passwords mysql --secret-file .chef/encrypted_data_bag_secret
id:       mysql
password: secret

You can set encrypted_data_bag_secret in knife.rb file:

cookbook_path    ["cookbooks", "site-cookbooks"]
node_path        "nodes"
role_path        "roles"
environment_path "environments"
data_bag_path    "data_bags"
encrypted_data_bag_secret ".chef/encrypted_data_bag_secret"

knife[:berkshelf_path] = "cookbooks"

and in this case no need to define secret-file for knife data bag commands:

$ knife solo data bag show passwords mysql
id:       mysql
password: secret

For Vagrant you should set encrypted_data_bag_secret_key_path:

...
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ...
    chef.encrypted_data_bag_secret_key_path = Chef::Config[:encrypted_data_bag_secret]
  ...
end

The Recipe DSL provides access to data bags and data bag items with the following methods: data_bag('bag'), where bag is the name of the data bag and data_bag_item('bag', 'item'), where bag is the name of the data bag and item is the name of the data bag item. Examples:

data_bag("pass")
# => ["mysql"]
item = data_bag_item("pass", "mysql")
item["password"]
# => "secret"

A recipe can access encrypted data bag items as long as the recipe is running on a node that has access to the shared-key that is required to decrypt the data. A secret can be specified by using the Chef::EncryptedDataBagItem.load method. For example:

mysql_creds = Chef::EncryptedDataBagItem.load("passwords", "mysql", secret_key)
mysql_creds["password"]
# => "secret"

where secret_key is the argument that specifies the location of the file that contains the encryption key. An encryption key can be configured so that the chef-client knows where to look using the Chef::Config[:encrypted_data_bag_secret] method, which defaults to /etc/chef/encrypted_data_bag_secret. When the default location is used, the argument that specifies the secret key file location is assumed to be the default and does not need to be explicitly specified in the recipe. For example:

mysql_creds = Chef::EncryptedDataBagItem.load("passwords", "mysql")
mysql_creds["password"]
# => "secret"

Summary

Chef Solo is a most simple way to begin working with Chef. Also it is very good choice, if your environment small (several servers) and you don’t need setup or buy separate Chef Server. But if you have huge numbers of servers or you don’t like limited functionality of Chef Solo, in this case you should thinking to setup or buy own Chef Server.

Chef Server

The Chef Server acts as a hub for configuration data. The server stores cookbooks, the policies that are applied to nodes, and metadata that describes each registered node that is being managed by the chef-client. Nodes use the chef-client to ask the server for configuration details, such as recipes, templates, and file distributions. The chef-client then does as much of the configuration work as possible on the nodes themselves (and not on the server). This scalable approach distributes the configuration effort throughout the organization.

The diagram [fig:overviewchefdraft] shows the relationships between the various elements of Chef, including the nodes, the server, and the workstations. These elements work together to provide the chef-client the information and instruction that it needs so that it can do its job.

[fig:overviewchefdraft]

We will learn Chef Server by practical examples in this chapter.

Installation

Exists several ways to install own Chef Server:

Of course, I prefer to use Chef Solo to install and configure Chef Server. Chef Solo will help us quickly deploy Chef Server on a new server, if something happens with it (crash file system of server, etc.). Do not forget to make a backups of Chef Server (because compared with Chef Solo, Chef Server will be the point of failure in your configuration management system).

Let’s create our folder, which will contain all our Chef kitchen:

$ mkdir my-server-cloud
$ cd my-server-cloud
$ cat Gemfile
source "https://rubygems.org"

gem 'chef'
gem 'berkshelf'
$ bundle
$ git clone https://github.com/opscode/chef-repo.git .
# or you can use "knife solo init .", if you will install knife-solo

To install and configure Chef Server exists cookbook chef-server. Let’s add this cookbook in Berkshelf:

source "http://api.berkshelf.com"

cookbook 'chef-server'

After running the command «berks install» this cookbook will be installed with dependencies.

$ berks install
Installing chef-server (2.0.1) from site: 'http://cookbooks.opscode.com/api/v1/cookbooks'
$ berks install --path cookbooks
Using chef-server (2.0.1)

Now we should configure a Chef Solo node for our Chef Server. From chapter «[sec:solo-node] » you should know how to define node.

{
  "fqdn": "10.33.33.33",
  "chef-server": {
    "api_fqdn": "10.33.33.33",
    "version": "latest",
    "configuration": {
      "notification_email": "notify@example.com",
      "chef-server-webui": {
        "enable": true
      }
    }
  },
  "run_list": [
    "recipe[chef-server]"
  ]
}

By configuration key you can change settings for Chef Server. All available setting, which can be redefined, you can find here. Our Chef Server by default takes your systems FQDN as Chef Server url, what is why I set «fqdn» in node IP 10.33.33.33, which will set to my server by Vagrant.

First, we should generate Vagrantfile:

$ vagrant init precise64
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

Next we need modeling a cluster of machines by Vagrant. Right now we need only chef server. Let’s modify Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

require 'chef'
require 'json'

Chef::Config.from_file(File.join(File.dirname(__FILE__), '.chef', 'knife.rb'))

chef_server_json = JSON.parse(Pathname(__FILE__).dirname.join('nodes', 'chef-server.example.com.json').read)

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  config.vm.define :chef_server do |chef_server|
    chef_server.vm.box = "precise64"
    chef_server.vm.network "private_network", ip: "10.33.33.33"

    chef_server.vm.provision :chef_solo do |chef|
      chef.cookbooks_path = Chef::Config[:cookbook_path]
      chef.roles_path = Chef::Config[:role_path]
      chef.data_bags_path = Chef::Config[:data_bag_path]
      chef.environments_path = Chef::Config[:environment_path]

      chef.run_list = chef_server_json.delete('run_list')
      chef.json = chef_server_json
    end
  end

end

You should have installed chef gem inside vagrant, as we did in chapter «[sec:solo-vagrant] » and install/update Chef Client inside server by command «knife solo prepare».

$ vagrant provision
[chef_server] Running provisioner: chef_solo...
Generating chef JSON and uploading...
Running chef-solo...
stdin: is not a tty
INFO: Forking chef instance to converge...
INFO: *** Chef 11.8.2 ***
INFO: Chef-client pid: 1831
INFO: Setting the run_list to ["recipe[chef-server]"] from JSON
INFO: Run List is [recipe[chef-server]]
INFO: Run List expands to [chef-server]
...

We can check Chef Server web interface by https://10.33.33.33 and info about libraries version by https://10.33.33.33/version url. It should looks like on figure [fig:chef-server-versions].

[fig:chef-server-versions]

I most cases web interface give information about your cloud, which you can get from knife tool. What is why generally it disabled by attribute «chef-server-webui.enable = false».

Next we should configure our knife to work with this Chef Server.

Knife

After installation Chef Server with default settings, Chef will generate pem keys, which will be used for knife and Chef clients for authentication with server. We should copy its from our Chef Server to «.chef» directory in project:

$ vagrant ssh chef_server
Welcome to Ubuntu 12.04.1 LTS (GNU/Linux 3.2.0-23-generic x86_64)

vagrant@precise64:~$ sudo cp /etc/chef-server/*.pem /vagrant/.chef/

On real (production) Chef Server you can use scp command.

Next we should create for knife configuration file. As you remember from chapter «[sec:solo-chef-folder] », we already have «.chef» folder with knife.rb config. But for Chef server we should define additional params. We can use for this configure command of knife:

$ knife configure -i
Overwrite .../my-server-cloud/.chef/knife.rb? (Y/N) y
Please enter the chef server URL: [https://macbookproleo:443] https://10.33.33.33
Please enter a name for the new user: [leo]
Please enter the existing admin name: [admin]
Please enter the location of the existing admin's private key: [/etc/chef-server/admin.pem] .chef/admin.pem
Please enter the validation clientname: [chef-validator]
Please enter the location of the validation key: [/etc/chef-server/chef-validator.pem] .chef/chef-validator.pem
Please enter the path to a chef repository (or leave blank):
Creating initial API user...
Please enter a password for the new user:
Created user[leo]
Configuration file written to .../my-server-cloud/.chef/knife.rb

Now our file look like this:

log_level                :info
log_location             STDOUT
node_name                'leo'
client_key               '.chef/leo.pem'
validation_client_name   'chef-validator'
validation_key           '.chef/chef-validator.pem'
chef_server_url          'https://10.33.33.33'
syntax_check_cache_path  '.chef/syntax_check_cache'

Let’s little modify it:

current_dir = File.dirname(__FILE__)

log_level                :info
log_location             STDOUT
node_name                'leo'
client_key               "#{current_dir}/leo.pem"
syntax_check_cache_path  "#{current_dir}/syntax_check_cache"
validation_client_name   "chef-validator"
validation_key           "#{current_dir}/chef-validator.pem"
chef_server_url          "https://10.33.33.33"
cookbook_path            ["#{current_dir}/../cookbooks", "#{current_dir}/../site-cookbooks"]
node_path                 "#{current_dir}/../nodes"
role_path                 "#{current_dir}/../roles"
data_bag_path             "#{current_dir}/../data_bags"
environment_path          "#{current_dir}/../environments"
#encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"

Let’s consider an options (part of this options already considered in chapter «[sec:solo-chef-folder]  »):

Now we check what knife can communicate with Chef server:

$ knife user list
admin
leo
$ knife client list
chef-validator
chef-webui

As you can see we successfully get list of users and clients from our Chef Server.

Bootstrap first node

Once the Chef Server workstation is configured, it can be used to install Chef on one (or more) nodes across the organization using a Knife bootstrap operation. The knife bootstrap command is used to SSH into the target machine, and then do what is needed to allow the chef-client to run on the node. It will install the chef-client executable (if necessary), generate keys, and register the node with the Chef Server. The bootstrap operation requires the IP address or FQDN of the target system, the SSH credentials (username, password or identity file) for an account that has root access to the node, and (if the operating system is not Ubuntu, which is the default distribution used by knife bootstrap) the operating system running on the target system.

First, let’s add new server in Vagrantfile:

...

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  ...

  config.vm.define :chef_first_client do |chef_client|
    chef_client.vm.box = "precise64"
    chef_client.vm.network "private_network", ip: "10.33.33.34"
  end

end

And reload vagrant servers:

$ vagrant halt chef_server
[chef_server] Attempting graceful shutdown of VM...
$ vagrant up
Bringing machine 'chef_server' up with 'virtualbox' provider...
Bringing machine 'chef_client' up with 'virtualbox' provider...
...

And now we can bootstrap node:

$ knife bootstrap localhost -x vagrant -p 2200 -i ~/.vagrant.d/insecure_private_key -N first.example.com --sudo
Bootstrapping Chef on localhost
localhost --2014-01-05 16:01:33--  https://www.opscode.com/chef/install.sh
...
localhost Chef Client finished, 0 resources updated
$ knife node list
first.example.com

And we can check what node created on server:

$ knife node list
first.example.com
$ knife client show first.example.com
admin:      false
chef_type:  client
json_class: Chef::ApiClient
name:       first.example.com
public_key: -----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----

validator:  false

Node in Vagrant

You can automate registration of node with your Chef Server in Vagrant. Let’s add new node in Vagrantfile:

...

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ...
  config.vm.define :chef_second_client do |chef_client|
    chef_client.vm.box = "precise64"
    chef_client.vm.network "private_network", ip: "10.33.33.35"
    chef_client.vm.provision :chef_client do |chef|
      chef.chef_server_url = Chef::Config[:chef_server_url]
      chef.validation_key_path = Chef::Config[:validation_key]
      chef.validation_client_name = Chef::Config[:validation_client_name]
      chef.node_name = 'second.example.com'

      chef.delete_node = true
      chef.delete_client = true
    end
  end

end

As you can see, options for «chef_client» the same as we set in knife.rb. After command vagrant up you can check what new node registered:

$ knife node list
first.example.com
second.example.com

When you provision your Vagrant virtual machine with Chef server, it creates a new Chef node entry and Chef client entry on the Chef server, using the hostname of the machine. After you tear down your guest machine, Vagrant can be configured to do it automatically with the following settings:

chef.delete_node = true
chef.delete_client = true

If you don’t specify it or set it to false, you must explicitly delete these entries from the Chef server before you provision a new one with Chef server. For example, using Chef’s built-in knife tool:

$ knife node delete second.example.com
$ knife client delete second.example.com

Example of vagrant output when destroy a node:

$ vagrant destroy chef_second_client
Are you sure you want to destroy the 'chef_second_client' VM? [y/N] y
[chef_second_client] Forcing shutdown of VM...
[chef_second_client] Destroying VM and associated drives...
[chef_second_client] Running cleanup tasks for 'chef_client' provisioner...
Deleting client "second.example.com" from Chef server...
Deleting node "second.example.com" from Chef server...
$ knife node list
first.example.com
$ knife client list
chef-validator
chef-webui
first.example.com

As you can see node and client removed from Chef server automatically.

Attributes

An attribute is a specific detail about a node. Attributes are used by the chef-client to understand:

As you read from previous sections of book, attributes can be defined in node, roles and environments, but it’s also can be defined by cookbooks.

Attribute Types

Attribute types can be any of the following:

At the beginning of a chef-client run, all default, override, and automatic attributes are reset. The chef-client rebuilds them using data collected by Ohai at the beginning of the chef-client run and by attributes that are defined in cookbooks, roles, and environments. Normal attributes are never reset. All attributes are then merged and applied to the node according to attribute precedence. At the conclusion of the chef-client run, all default, override, and automatic attributes disappear, leaving only a collection of normal attributes that will persist until the next chef-client run.

Automatic (Ohai)

An automatic attribute is a specific detail about a node, such as an IP address, a host name, a list of loaded kernel modules, and so on. Automatic attributes are detected by Ohai and are then used by the chef-client to ensure that these attribute are handled properly during every chef-client run. The most commonly accessed automatic attributes are:

The list of automatic attributes that are collected by Ohai at the start of each chef-client run vary from organization to organization, and will often vary between the various server types being configured and the platforms on which those servers are run. All attributes collected by Ohai are unmodifiable by the chef-client.

Attribute Precedence

Attribute types can be any of the following:

  1. A default attribute located in a cookbook attribute file

  2. A default attribute located in a recipe

  3. A default attribute located in an environment

  4. A default attribute located in role

  5. A force_default attribute located in a cookbook attribute file

  6. A force_default attribute located in a recipe

  7. A normal attribute located in a cookbook attribute file

  8. A normal attribute located in a recipe

  9. An override attribute located in a cookbook attribute file

  10. An override attribute located in a recipe

  11. An override attribute located in a role

  12. An override attribute located in an environment

  13. A force_override attribute located in a cookbook attribute file

  14. A force_override attribute located in a recipe

  15. An automatic attribute identified by Ohai at the start of the chef-client run

where the last attribute in the list is the one that is applied to the node.

Attribute precedence, viewed from the same perspective as the overview diagram [fig:overviewchefattributesprecedence], where the numbers in the diagram match the order of attribute precedence:

[fig:overviewchefattributesprecedence]

Attribute precedence [fig:overviewchefattributestable], when viewed as a table:

[fig:overviewchefattributestable]

Role

Role work in the same way, as in Chef Solo (chapter «[sec:solo-role] »). For example, we will install on «first.example.com» nginx. First of all, we need add nginx cookbook in Berksfile:

source "http://api.berkshelf.com"

cookbook 'chef-server'
cookbook 'nginx'
cookbook 'yum', '~> 3.0'

After command berks install we will create «nginx» role:

{
  "name": "nginx",
  "description": "The base role for systems that serve web server",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "default_attributes": {
    "nginx": {
      "install_method": "source",
      "version": "1.6.0",
      "default_site_enabled": true,
      "source": {
        "url": "http://nginx.org/download/nginx-1.6.0.tar.gz"
      },
      "worker_rlimit_nofile": 30000,
      "worker_connections": 4000
    }
  },
  "run_list": [
    "recipe[nginx]"
  ]
}

And «first.example.com» node:

{
  "name": "first.example.com",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "normal": {
    "fqdn": "10.33.33.34"
  },
  "default": {},
  "override": {},
  "run_list": [
    "role[nginx]"
  ]
}

As you can see, node have different format, than Chef Solo. Keys normal, default and override contain attributes. Difference of this attributes you can read in chapter «[sec:server-attributes] ». Now we should upload cookbooks, role and node in Chef Server. For this we can use knife and berkshelf:

// upload all cookbooks from path 'cookbooks' and 'site-cookbooks' (use --force if cookbook frozen).
// But for vendor cookbooks before you need execute 'berks install --path cookbooks'
$ knife cookbook upload -a
// upload selected cookbook
$ knife cookbook upload nginx
// or upload all cookbooks by berks
$ berks upload
// create/update role from file
$ knife role from file roles/nginx.json
// update node from file
$ knife node from file nodes/first.example.com.json

Next we should use knife ssh command to do something on nodes. The knife ssh subcommand is used to invoke SSH commands (in parallel) on a subset of nodes within an organization, based on the results of a search query. Example:

// execute on 'first.example.com' chef-client
$ knife ssh 'name:first.example.com' 'sudo chef-client' -i ~/.vagrant.d/insecure_private_key -x vagrant
10.33.33.34 Starting Chef Client, version 11.8.2
10.33.33.34 resolving cookbooks for run list: ["nginx::source"]
...
10.33.33.34 Recipe: nginx::source
10.33.33.34   * service[nginx] action nothing (up to date)
10.33.33.34   * service[nginx] action reload
10.33.33.34     - reload service service[nginx]
10.33.33.34
10.33.33.34 Chef Client finished, 4 resources updated

Second time you run this command can cause errors like «Failed to connect to». It is because FQDN set by Ohai in chef-client will not visible hostname (in my example nodes have «precise64» hostname). In real cluster you will not have such problems, because you will use real hostnames. But in our case we can use Vagrant «chef_client» stuff. Let’s create node «second.example.com» and upload it on server:

{
  "name": "second.example.com",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "normal": {
    "fqdn": "10.33.33.35"
  },
  "default": {},
  "override": {},
  "run_list": [
    "role[nginx]"
  ]
}
$ knife node from file nodes/second.example.com.json

Now you can just run vagrant provision for second node:

$ vagrant provision chef_second_client
[chef_second_client] Running provisioner: chef_client...
Creating folder to hold client key...
Uploading chef client validation key...
Generating chef JSON and uploading...
[chef_second_client] Warning: Chef run list is empty. This may not be what you want.
Running chef-client...
stdin: is not a tty
INFO: Forking chef instance to converge...
INFO: *** Chef 11.8.2 ***
INFO: Chef-client pid: 1198
...
INFO: Running report handlers
INFO: Report handlers complete

It will run chef-client on server, which will get all needed info how to «cook» node from Chef Server. After this you can check what on url http://10.33.33.35/ running nginx.

Environment

Environment in Chef Server is similar to Chef Solo (chapter «[sec:solo-environment] »). Except default attributes, environment in Chef Server can contain cookbook_versions attribute. In this attribute you can lock cookbook versions. Example:

{
  "name": "development",
  "description": "development environment",
  "chef_type": "environment",
  "json_class": "Chef::Environment",
  "default_attributes": {},
  "cookbook_versions": {
    "nginx": "= 2.2.0"
  }
}

As you can see we locked nginx to 2.2.0 version for development environment. This is very useful stuff, because when released new versions of some cookbooks, you need check what you can update it to new version without break a production environment. In this case you can change version of cookbooks inside cookbook_versions in your test (development, staging, etc.) environment, check what all work fine with new cookbook and update production environment only in success case.

Set environment to node similar to Chef Solo, but in this case you should use chef_environment attribute. Example:

{
  "name": "second.example.com",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "chef_environment": "development",
  "normal": {
    "fqdn": "10.33.33.35"
  },
  "default": {},
  "override": {},
  "run_list": [
    "role[nginx]"
  ]
}

To check what all work fine, first of all you should upload you environment and update node in Chef Server:

$ knife environment from file environments/development.json
Updated Environment development

$ knife node from file nodes/second.example.com.json
Updated Node second.example.com!

And check what all work fine by command vagrant provision for second node:

$ vagrant provision chef_second_client
[chef_second_client] Running provisioner: chef_client...
Creating folder to hold client key...
Uploading chef client validation key...
Generating chef JSON and uploading...
...

Knife ssh

The knife ssh subcommand is used to invoke SSH commands (in parallel) on a subset of nodes within an organization, based on the results of a search query. We already use it to run chef-client on «first.example.com» node. Let’s consider an examples.

To find the uptime of all of web servers (all node, which have role «web»):

$ knife ssh "role:web" "uptime" -i ../keys/production.pem -x ubuntu
***.com    13:18:28 up 55 days, 14 min,  1 user,  load average: 0.00, 0.01, 0.05
***.com    13:18:28 up 75 days, 23:49,  1 user,  load average: 0.00, 0.01, 0.05
***.com    13:18:28 up 55 days, 13 min,  1 user,  load average: 0.08, 0.03, 0.05

To run the chef-client on all nodes:

$ knife ssh 'name:*' 'sudo chef-client' -i ../keys/production.pem -x ubuntu
...

To run the chef-client on all nodes, which name begin from «second» string:

$ knife ssh 'name:second*' 'sudo chef-client' -i ../keys/production.pem -x ubuntu
...

To upgrade all nodes (don’t do this on real production nodes):

$ knife ssh 'name:*' 'sudo aptitude upgrade -y' -i ../keys/production.pem -x ubuntu
...

To get memory information from all nodes in staging environment:

$ knife ssh "chef_environment:staging" "free -m" -i ../keys/production.pem -x ubuntu
***.com              total       used       free     shared    buffers     cached
***.com Mem:          1692       1182        509          0        181        491
***.com -/+ buffers/cache:        509       1183
***.com Swap:          895          6        889
...

Chef-client cookbook

Sometimes you may want update you servers automatically (instead using knife ssh). For example, you just updated new cookbooks, roles and nodes and all nodes should automatically fetch new cookbooks and apply changes if something updated (and for you not critical update speed). We can use special cookbook chef-client for this. It allow for use bluepill, daemontools, runit or cron to configure your systems to run Chef Client as a service. First of all we need add this cookbook in Berksfile and run berks install:

source "http://api.berkshelf.com"

cookbook 'chef-server'
cookbook 'nginx'
cookbook 'yum', '~> 3.0'
cookbook 'chef-client'

Next we will create new node:

{
  "name": "chef-client",
  "description": "The base role for chef-client",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "default_attributes": {
    "chef_client": {
      "interval": 1800,
      "init_style": "upstart",
      "config": {
        "client_fork": true
      }
    }
  },
  "run_list": [
    "recipe[chef-client]",
    "recipe[chef-client::config]"
  ]
}

We set by attributes to check Chef each 1800 sec (30 min) and use «fork» method for execution of chef-client.

Next add chef-client in node run_list:

{
  "name": "second.example.com",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "chef_environment": "development",
  "normal": {
    "fqdn": "10.33.33.35"
  },
  "default": {},
  "override": {},
  "run_list": [
    "role[chef-client]",
    "role[nginx]"
  ]
}

After upload coobooks, role and nodes on Chef Server, we can check what chef-client will be added to upstart:

$ vagrant provision chef_second_client
[chef_second_client] Running provisioner: chef_client...
Creating folder to hold client key...
Uploading chef client validation key...
...
INFO: service[chef-client] restarted
INFO: Chef Run complete in 9.599237616 seconds
INFO: Running report handlers
INFO: Report handlers complete

By command knife status you can check chef-client status on nodes:

$ knife status
3 minutes ago, first.example.com, precise64, 10.0.2.14, ubuntu 12.04.
2 minutes ago, second.example.com, precise64, 10.0.2.15, ubuntu 12.04.

Of course, you can filter this list, if you have too many nodes (filter by role, environment, etc.):

$ knife status 'name:second*'
4 minutes ago, second.example.com, precise64, 10.0.2.15, ubuntu 12.04.
$ knife status 'role:nginx'
6 minutes ago, first.example.com, precise64, 10.0.2.14, ubuntu 12.04.
5 minutes ago, second.example.com, precise64, 10.0.2.15, ubuntu 12.04.

Data bags

Data bags work similar as in Chef Solo (chapter «[sec:solo-data-bag] »). But in your recipe you can use search command to search data bags on servers. Any search for a data bag (or a data bag item) must specify the name of the data bag and then provide the search query string that will be used during the search. For example, to use Knife to search within a data bag named «admin_data» across all items, except for the «admin_users» item, enter the following:

$ knife search admin_data "(NOT id:admin_users)"

Or, to include the same search query in a recipe, use a code block similar to:

search(:admin_data, "NOT id:admin_users")

It may not be possible to know which data bag items will be needed. It may be necessary to load everything in a data bag (but not know what «everything» is). Using a search query is the ideal way to deal with that ambiguity, yet still ensure that all of the required data is returned. The following examples show how a recipe can use a series of search queries to search within a data bag named «admins». For example, to find every administrator:

search(:admins, "*:*")

Or to search for an administrator named «charlie»:

search(:admins, "id:charlie")

Or to search for an administrator with a group identifier of «ops»:

search(:admins, "gid:ops")

Or to search for an administrator whose name begins with the letter «c»:

search(:admins, "id:c*")

Data bag items that are returned by a search query can be used as if they were a hash. For example:

charlie = search(:admins, "id:charlie").first
# => variable 'charlie' is set to the charlie data bag item
charlie["gid"]
# => "ops"
charlie["shell"]
# => "/bin/zsh"

Summary

Chef is a systems and cloud infrastructure automation framework that makes it easy to deploy servers and applications to any physical, virtual, or cloud location, no matter the size of the infrastructure.

Writing Cookbooks

A cookbook is the fundamental unit of configuration and policy distribution. Each cookbook defines a scenario, such as everything needed to install and configure MySQL, and then it contains all of the components that are required to support that scenario.

As you read from previous chapter vendor cookbooks can help you to install and configure any possible software, but in most cases it is not enough. This is because you have your application, which need install, configure special cases only for this application. What is why you must to know how to write own Chef cookbooks.

Chef cookbooks is written on Ruby language. It is dynamic and open source programming language, which very well fit to use as DSL for Chef recipes. Before you start reading this chapter, you should know Ruby at least basic stuff (Ruby types, loops, conditions, ERB, etc).

Cookbook file organization

For beginning we will generate cookbook by knife or berks. You can install both by bundler (we already did this in our Chef server kitchens). So, let’s create «my_cool_app» cookbook inside «site-cookbooks» dir:

$ cd site-cookbooks
$ knife cookbook create my_cool_app -o .
# or another way by berks
$ berks cookbook my_cool_app

After this you should see inside «site-cookbooks» new folder «my_cool_app». This is our cookbook, which have such file structure inside:

$ ls -l my_cool_app
total 72
drwxr-xr-x  .
drwxr-xr-x  ..
drwxr-xr-x  .git
-rw-r--r--  .gitignore
-rw-r--r--  Berksfile
-rw-r--r--  Gemfile
-rw-r--r--@ LICENSE
-rw-r--r--  README.md
-rw-r--r--  Thorfile
-rw-r--r--  Vagrantfile
drwxr-xr-x  attributes
-rw-r--r--  chefignore
drwxr-xr-x  definitions
drwxr-xr-x  files
drwxr-xr-x  libraries
-rw-r--r--  metadata.rb
drwxr-xr-x  providers
drwxr-xr-x  recipes
drwxr-xr-x  resources
drwxr-xr-x  templates

Let’s consider this structure:

Metadata

Metadata is file, which contain all main information about cookbook. Let’s consider our generated example:

name             'my_cool_app'
maintainer       'YOUR_NAME'
maintainer_email 'YOUR_EMAIL'
license          'All rights reserved'
description      'Installs/Configures my_cool_app'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '0.1.0'

This file written on Ruby and can have such settings:

We need modify it to set our information about this cookbook:

name             'my_cool_app'
maintainer       'Alexey Vasiliev'
maintainer_email 'leopard_ne@inbox.ru'
license          'MIT'
description      'Installs/Configures my_cool_app'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '0.1.0'

As of writing this cookbook, we will be adding information to this file on it.

Resources and Providers

As you read from previous chapter, Chef inside have resources (in example we used package resource). A resource defines the actions that can be taken, such as when a package should be installed, whether a service should be enabled or restarted, which groups, users, or groups of users should be created, where to put a collection of files, what the name of a new directory should be, and so on. During a chef-client run, each resource is identified and then associated with a provider. The provider then does the work to complete the action defined by the resource. Each resource is processed in the same order as they appear in a recipe. The chef-client ensures that the same actions are taken the same way everywhere and that actions produce the same result every time. A resource is implemented within a recipe using Ruby.

Let’s look at the most necessary resources.

Bash

The bash resource is used to execute scripts using the Bash interpreter and includes all of the actions and attributes that are available to the execute resource. Example:

bash "install_something" do
  user "root"
  cwd "/tmp"
  code «-EOH
  wget http://www.example.com/tarball.tar.gz
  tar -zxf tarball.tar.gz
  cd tarball
  ./configure
  make
  make install
  EOH
end

Cron

The cron resource is used to manage cron entries for time-based job scheduling. Attributes for a schedule will default to * if not provided. The cron resource requires access to a crontab program, typically cron. Example:

cron "name_of_cron_entry" do
  hour "8"
  weekday "6"
  mailto "admin@opscode.com"
  action :create
end
cron "noop" do
  hour "5"
  minute "0"
  command "/bin/true"
end

Directory

The directory resource is used to manage a directory, which is a hierarchy of folders that comprises all of the information stored on a computer. The root directory is the top-level, under which the rest of the directory is organized. The directory resource uses the name attribute to specify the path to a location in a directory. Typically, permission to access that location in the directory is required. Example:

directory "/tmp/something" do
  owner "root"
  group "root"
  mode 00755
  action :create
end
%w{dir1 dir2 dir3}.each do |dir|
  directory "/tmp/mydirs/#{dir}" do
    mode 00775
    owner "root"
    group "root"
    action :create
    recursive true
  end
end

Git

The git resource is used to manage source control resources that exist in a git repository. git version 1.6.5 (or higher) is required to use all of the functionality in the git resource. Example:

git "/opt/mysources/couch" do
  repository "git://git.apache.org/couchdb.git"
  reference "master"
  action :sync
end
git "#{Chef::Config[:file_cache_path]}/ruby-build" do
 repository "git://github.com/sstephenson/ruby-build.git"
 reference "master"
 action :sync
end

bash "install_ruby_build" do
 cwd "#{Chef::Config[:file_cache_path]}/ruby-build"
 user "rbenv"
 group "rbenv"
 code «-EOH
   ./install.sh
   EOH
 environment 'PREFIX' => "/usr/local"
end

The link resource is used to create symbolic or hard links. Example:

link "/tmp/passwd" do
  to "/etc/passwd"
end
link "/tmp/passwd" do
  to "/etc/passwd"
  link_type :hard
end

Cookbook_file

The cookbook_file resource is used to transfer files from a sub-directory of the files/ directory in a cookbook to a specified path that is located on the host running the chef-client or chef-solo. The file in a cookbook is selected according to file specificity, which allows different source files to be used based on the hostname, host platform (operating system, distro, or as appropriate), or platform version. Files that are located under COOKBOOK_NAME/files/default can be used on any platform. Example:

cookbook_file "/tmp/testfile" do
  source "testfile"
  mode 00644
end
cookbook_file "/etc/yum.repos.d/custom.repo" do
  source "custom"
  mode 00644
  notifies :run, "execute[create-yum-cache]", :immediately
  notifies :create, "ruby_block[reload-internal-yum-cache]", :immediately
end

Template

The template resource is used to manage file contents with an embedded Ruby (erb) template. This resource includes actions and attributes from the file resource. Template files managed by the template resource follow the same file specificity rules as the remote_file and file resources. Example:

template "/tmp/config.conf" do
  source "config.conf.erb"
end
template "/tmp/somefile" do
  mode 00644
  source "somefile.erb"
  not_if {File.exists?("/etc/passwd")}
end

Script

The script resource is used to execute scripts using the specified interpreter (Bash, Csh, Perl, Python, or Ruby) and includes all of the actions and attributes that are available to the execute resource. Example:

script "install_something" do
  interpreter "bash"
  user "root"
  cwd "/tmp"
  code «-EOH
  wget http://www.example.com/tarball.tar.gz
  tar -zxf tarball.tar.gz
  cd tarball
  ./configure
  make
  make install
  EOH
end

User

The user resource is used to add users, update existing users, remove users, and to lock/unlock user passwords. Example:

user "random" do
  supports :manage_home => true
  comment "Random User"
  uid 1234
  gid "users"
  home "/home/random"
  shell "/bin/bash"
  password "$1$JJsvHslV$szsCjVEroftprNn4JHtDi."
end

There are a number of encryption options and tools that can be used to create a password shadow hash. In general, using a strong encryption method like SHA-512 and the passwd command in the OpenSSL toolkit is a good approach, however the encryption options and tools that are available may be different from one distribution to another. The following examples show how the command line can be used to create a password shadow hash. When using the passwd command in the OpenSSL tool:

$ openssl passwd -1 "theplaintextpassword"

When using mkpasswd:

$ mkpasswd -m sha-512

Another example:

user "systemguy" do
  comment "system guy"
  system true
  shell "/bin/false"
end

Deploy

The deploy resource is used to manage and control deployments. This is a popular resource, but is also complex, having the most attributes, multiple providers, the added complexity of callbacks, plus four attributes that support layout modifications from within a recipe.

The deploy resource is modeled after Capistrano, a utility and framework for executing commands in parallel on multiple remote machines via SSH. The deploy resource is designed to behave in a way that is similar to the deploy and deploy:migration tasks in Capistrano.

Recipes

Any cookbook contains recipes. The default recipe inside cookbook have name «default». Let’s add our default recipe, which will install git:

#
# Cookbook Name:: my_cool_app
# Recipe:: default
#
# Copyright (C) 2014 Alexey Vasiliev
#
# MIT
#

package 'git'

As you can see, at the beginning of recipe we have comments about this recipe. Next we add resource «package» with argument «git». The «package» resource is used to manage packages on the system. For example, on Debian or Ubuntu resource «package» will use «apt-get» command to install git on system.

Now you should add «my_cool_app» into run-list to use this cookbook:

{
  "name": "second.example.com",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "chef_environment": "development",
  "normal": {
    "fqdn": "10.33.33.35"
  },
  "default": {},
  "override": {},
  "run_list": [
    "role[chef-client]",
    "role[nginx]",
    "recipe[my_cool_app]"
  ]
}

If you using Chef Server, don’t forget upload this cookbook and update node on Chef Server by knife.

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.
$ knife node from file nodes/second.example.com.json
Updated Node second.example.com!
// on real environment you will execute "knife ssh 'name:second.example.com' 'sudo chef-client' -i ../keys/production.pem -x ubuntu"
$ vagrant provision chef_second_client
INFO: Chef Run complete in 26.935610739 seconds
INFO: Running report handler

Let’s install also ntp package in the same recipe. Because we have in recipe Ruby syntax, we can little DRY our code:

%w(git ntp).each do |pack|
  package pack
end

Again upload cookbook and run chef-client:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.
// on real environment you will execute "knife ssh 'name:second.example.com' 'sudo chef-client' -i ../keys/production.pem -x ubuntu"
$ vagrant provision chef_second_client
INFO: Chef Run complete in 26.935610739 seconds
INFO: Running report handler
$ vagrant ssh chef_second_client
...
vagrant@precise64:~$ ps ax | grep ntp
 1115 ?        Ss     0:00 /usr/sbin/ntpd -p /var/run/ntpd.pid -g -u 103:108
13839 pts/2    S+     0:00 grep --color=auto ntp
vagrant@precise64:~$ git --version
git version 1.7.9.5

As you can see our simple cookbook is working.

Assign Dependencies

If a cookbook has a dependency on a recipe that is located in another cookbook, that dependency must be declared in the metadata.rb file for that cookbook using the depends keyword.

For example, if the following recipe is included in a cookbook named «my_app»:

include_recipe "apache2::mod_ssl"

Then the metadata.rb file for that cookbook would have:

depends "apache2"

Create Exceptions

A recipe can write events to a log file and can cause exceptions using Chef::Log. The levels include debug, info, warn, error, and fatal. For example, to just capture information:

Chef::Log.info('some useful information')

Or to trigger a fatal exception:

Chef::Log.fatal!('something bad')

Include Recipes

A recipe can include one (or more) recipes located in external cookbooks by using the include_recipe method. When a recipe is included, the resources found in that recipe will be inserted (in the same exact order) at the point where the include_recipe keyword is located. The syntax for including a recipe is like this:

include_recipe "recipe"

For example:

include_recipe "apache2::mod_ssl"

If the include_recipe method is used more than once to include a recipe, only the first inclusion is processed and any subsequent inclusions are ignored.

Reload Attributes

Attributes sometimes depend on actions taken from within recipes, so it may be necessary to reload a given attribute from within a recipe. For example:

ruby_block 'some_code' do
  block do
    node.from_file(run_context.resolve_attribute("COOKBOOK_NAME", "ATTR_FILE"))
  end
  action :nothing
end

Accessor Methods

Attribute accessor methods are automatically created and the method invocation can be used interchangeably with the keys. For example:

default.apache.dir          = "/etc/apache2"
default.apache.listen_ports = [ "80","443" ]

This is a matter of style and preference for how attributes are reloaded from recipes, and may be seen when retrieving the value of an attribute.

Attributes

An attribute can be defined in a cookbook (or a recipe) and then used to override the default settings on a node. When a cookbook is loaded during a chef-client run, these attributes are compared to the attributes that are already present on the node. When the cookbook attributes take precedence over the default attributes, the chef-client will apply those new settings and values during the chef-client run on the node.

An attribute file is located in the attributes/ sub-directory for a cookbook. When a cookbook is run against a node, the attributes contained in all attribute files are evaluated in the context of the node object. Node methods (when present) are used to set attribute values on a node. For example, the «apache2» cookbook contains an attribute file called default.rb, which contains the following attributes:

default["apache"]["dir"]          = "/etc/apache2"
default["apache"]["listen_ports"] = [ "80","443" ]

The use of the node object (node) is implicit in the previous example; the following example defines the node object itself as part of the attribute:

node.default["apache"]["dir"]          = "/etc/apache2"
node.default["apache"]["listen_ports"] = [ "80","443" ]

In our cookbook «my_cool_app» we want create directory for web app, add to this directory html file, generate config for nginx and enable this configuration. Let’s add all this by using Chef attributes and resources.

default['my_cool_app']['web_dir']       = '/var/www/my_cool_app'
default['my_cool_app']['user']          = 'vagrant'
default['my_cool_app']['name']          = 'my_cool_app'
# install needed package
%w(git ntp).each do |pack|
  package pack
end

# create directory for web app
directory node['my_cool_app']['web_dir'] do
  owner node['my_cool_app']['user']
  mode "0755"
  recursive true
end

# upload index.html file to web app directory as index.html
cookbook_file "#{node['my_cool_app']['web_dir']}/index.html" do
  owner node['my_cool_app']['user']
  source "index.html"
  mode 0755
end

# create nginx config from temlate nginx.conf.erb
nginx_config = "#{node['nginx']['dir']}" +
  "/sites-available/#{node['my_cool_app']['name']}.conf"
template nginx_config do
  source "nginx.conf.erb"
  mode "0644"
end

# activate nginx.conf in nginx
nginx_site "#{node['my_cool_app']['name']}.conf"
server {
    listen 80 default;
    charset utf-8;
    root <%= node['my_cool_app']['web_dir'] %>;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <title>My cool app</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, maximum-scale=1.0" />
</head>
<body>
  <h1>This is my cool web app</h1>
</body>
</html>
name             'my_cool_app'
maintainer       'Alexey Vasiliev'
maintainer_email 'leopard_ne@inbox.ru'
license          'MIT'
description      'Installs/Configures my_cool_app'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '0.1.0'

recipe 'my_cool_app',   'Configure my cool app'

depends 'nginx',           '~> 2.2.0'

As you can see, we add default attributes for recipe. Better to use for cookbook attribute name as root for all attributes - in this case you will not have problem, if several cookbooks will use similar keys for settings. As you can see all attributes of this cookbook located inside «my_cool_app» attribute. In recipe we add needed commands to create and activate our index.html. Also we add to metadata information about recipe and dependence to nginx cookbook (because we used inside our recipe «nginx_site» resource). After upload cookbook at Chef server and run chef-client at the second node, we can see results at http://10.33.33.35 (Pic [fig:mycoolappindex]):

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.
// on real environment you will execute "knife ssh 'name:second.example.com' 'sudo chef-client' -i ../keys/production.pem -x ubuntu"
$ vagrant provision chef_second_client
...
INFO: directory[/var/www/my_cool_app] created directory /var/www/my_cool_app
INFO: directory[/var/www/my_cool_app] owner changed to 1000
INFO: directory[/var/www/my_cool_app] mode changed to 755
INFO: cookbook_file[/var/www/my_cool_app/index.html] created file /var/www/my_cool_app/index.html
INFO: cookbook_file[/var/www/my_cool_app/index.html] updated file contents /var/www/my_cool_app/index.html
INFO: cookbook_file[/var/www/my_cool_app/index.html] owner changed to 1000
INFO: cookbook_file[/var/www/my_cool_app/index.html] mode changed to 755
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] created file /etc/nginx/sites-available/my_cool_app.conf
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] updated file contents /etc/nginx/sites-available/my_cool_app.conf
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] mode changed to 644
INFO: execute[nxensite my_cool_app.conf] ran successfully
INFO: execute[nxensite my_cool_app.conf] sending reload action to service[nginx] (delayed)
INFO: service[nginx] reloaded
...

[fig:mycoolappindex]

Templates

As you can see in previous chapter we used resource template for generate nginx config in default recipe. A cookbook template is a file written in a markup language that allows the contents of a file to be dynamically generated based on variables or complex logic. Templates can contain Ruby expressions and statements. Templates are a great way to manage configuration files across an organization. A template requires a template resource being added to a recipe and then a corresponding Embedded Ruby (ERB) template being added to a cookbook.

To use a template, two things must happen:

For example, the following template file and template resource settings can be used to manage a configuration file named /etc/sudoers. Within a cookbook that uses sudo, the following resource could be added to recipes/default.rb:

template "/etc/sudoers" do
  source "sudoers.erb"
  mode 0440
  owner "root"
  group "root"
  variables({
     :sudoers_groups => node[:authorization][:sudo][:groups],
     :sudoers_users => node[:authorization][:sudo][:users]
  })
end

And then create a template called sudoers.erb and save it to templates/default/sudoers.erb:

#
# /etc/sudoers
#
# Generated by Chef for <%= node[:fqdn] %>
#

Defaults        !lecture,tty_tickets,!fqdn

# User privilege specification
root          ALL=(ALL) ALL

<% @sudoers_users.each do |user| -%>
<%= user %>   ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL
<% end -%>

# Members of the sysadmin group may gain root privileges
%sysadmin     ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL

<% @sudoers_groups.each do |group| -%>
# Members of the group '<%= group %>' may gain root privileges
%<%= group %> ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL
<% end -%>

And then set the default attributes in attributes/default.rb:

default["authorization"]["sudo"]["groups"] = [ "sysadmin","wheel","admin" ]
default["authorization"]["sudo"]["users"]  = [ "jerry","greg"]

When a template is rendered, Ruby expressions and statements are evaluated by the chef-client. The variables listed in the resource’s variables parameter and the node object are evaluated. The chef-client then passes these variables to the template, where they will be accessible as instance variables within the template; the node object can be accessed just as if it were part of a recipe, using the same syntax.

For example, a simple template resource like this:

node[:fqdn] = "latte"
template "/tmp/foo" do
  source "foo.erb"
  variables({
    :x_men => "are keen"
  })
end

And a simple Embedded Ruby (ERB) template like this:

The node <%= node[:fqdn] %> thinks the x-men <%= @x_men %>

Would render something like:

The node latte thinks the x-men are keen

Even though this is a very simple example, the full capabilities of Ruby can be used to tackle even the most complex and demanding template requirements.

File Specificity

A cookbook will frequently be designed to work across many platforms and will often be required to distribute a specific file to a specific platform. A cookbook can be designed to support distributing files across platforms, but ensuring that the right file ends up on each system.

The pattern for file specificity is as follows:

  1. host-node[:fqdn]

  2. node[:platform]-node[:platform_version]

  3. node[:platform]-version_components: The version string is split on decimals and searched from greatest specificity to least; for example, if the location from the last rule was centos-5.7.1, then centos-5.7 and centos-5 would also be searched.

  4. node[:platform]

  5. default

The naming of folders within cookbook directories must literally match the host notation used for template specificity matching. For example, if a host is named «foo.example.com», then the folder must be named «host-foo.example.com».

A cookbook may have a /templates directory structure like this:

templates/
  windows-6.2
  windows-6.1
  windows-6.0
  windows
  default

and a resource that looks something like the following:

template "C:\path\to\file\text_file.txt" do
  source "text_file.txt"
  mode 0755
  owner "root"
  group "root"
end

This resource would be matched in the same order as the /templates directory structure. For a node named «host-node-desktop» that is running Windows 7, the second item would be the matching item and the location:

/templates
  windows-6.2/text_file.txt
  windows-6.1/text_file.txt
  windows-6.0/text_file.txt
  windows/text_file.txt
  default/text_file.txt

Partial Templates

A template can be built in a way that allows it to contain references to one (or more) smaller template files. (These smaller template files are also referred to as partials.) A partial can be referenced from a template file in one of the following ways:

Use the render method in a template to reference a partial template file with the following syntax:

<%= render "partial_name.txt.erb", :option => {} %>

where partial_name.txt.erb is the name of the partial template file and :option is one (or more) of the following options:

For example:

<%= render "simple.txt.erb", :variables => {:user => @user }, :local => true %>

LWRPs

A LWRP (Lightweight Resources and Providers) is a part of a cookbook that is used to extend the chef-client in a way that allows custom actions to be defined, and then used in recipes in much the same way as any platform resource. A LWRP has two principal components:

In addition, most lightweight providers are built using platform resources and some lightweight providers are built using custom Ruby code.

Once created, a LWRP becomes a Ruby class within the organization. During each chef-client run, the chef-client will read the lightweight resources from recipes and process them alongside all of the other resources. When it is time to configure the node, the chef-client will use the corresponding lightweight provider to determine the steps required to bring the system into the desired state.

Where the lightweight resource represents a piece of the system, its current state, and the action that is needed to move it to the desired state, a lightweight provider defines the steps that are required to bring that piece of the system from its current state to the desired state. A LWRP behaves similar to platform resources and providers:

Lightweight resources and providers are loaded from files that are saved in the following cookbook sub-directories:

Directory Description
providers/ The sub-directory in which lightweight providers are located.
resources/ The sub-directory in which lightweight resources are located.

The naming patterns of lightweight resources and providers are determined by the name of the cookbook and by the name of the files in the «resources/» and «providers/» sub-directories. For example, if a cookbook named «example» was downloaded to the chef-repo, it would be located at «/cookbooks/example/». If that cookbook contained two resources and two providers, the following files would be part of the «resources/» directory:

Files Resource Name Generated Class
default.rb example Chef::Resource::Example
custom.rb example_custom Chef::Resource::ExampleCustom

And the following files would be part of the «providers/» directory:

Files Provider Name Generated Class
default.rb example Chef::Provider::Example
custom.rb custom Chef::Provider::ExampleCustom

Let’s add in our «my_cool_app» LWRP, which will add in /etc/ssh/ssh_known_hosts host.

Resources

First of all we should create directory «resources» and add to it know_host.rb file with content:

actions :create, :delete
default_action :create

attribute :host, :kind_of => String, :name_attribute => true, :required => true
attribute :key, :kind_of => String
attribute :port, :kind_of => Fixnum, :default => 22
attribute :known_hosts_file, :kind_of => String, :default => '/etc/ssh/ssh_known_hosts'

# Needed for Chef versions < 0.10.10
def initialize(*args)
  super
  @action = :create
end

Let’s take it line by line. The first line specifies the allowed actions. Actions are what your resource can do, e.g. start, stop, create, delete, etc. In this case, you can :create or :delete known host. The next line defines the default_action for our resource, in this case :create. If you don’t specify an action when you use the resource in a recipe, it will default to creating a known host, which is what you probably want. A general philosophy of Chef is to define intelligent or «sane» defaults.

Lines 4-7 define attributes, or properties of the known host resource we are creating. Line 4 defines an :host attribute. Its :name_attribute is true, which means that this attribute will be set to the string between my_cool_app_know_host and do. Example:

my_cool_app_know_host "Add github host" do
  host 'github.com'
end

my_cool_app_know_host 'github.com' do
  # The :host attribute will be set to 'github.com'
end

In the second example above, the :host attribute wll be set to «github.com».

Also, on line 4, we are defining the kind_of validation parameter to tell the resource which kind of data we should expect (in this case, a string), whether this attribute is required (yes). Line 5 defines a :key attribute, which is an optional string with no default. Line 6 defines a :port attribute, a Ruby Fixnum (i.e. an integer) with a default of 22, which is the default when you create a known host. Line 7 defines a :known_hosts_file attribute, a string with a default of /etc/ssh/ssh_known_hosts, which is the default file with known hosts for ssh client.

For example, the cron_d lightweight resource (found in the cron cookbook) can be used to manage files located in /etc/cron.d:

actions :create, :delete
default_action :create

attribute :name, :kind_of => String, :name_attribute => true
attribute :cookbook, :kind_of => String, :default => "cron"
attribute :minute, :kind_of => [Integer, String], :default => "*"
attribute :hour, :kind_of => [Integer, String], :default => "*"
attribute :day, :kind_of => [Integer, String], :default => "*"
attribute :month, :kind_of => [Integer, String], :default => "*"
attribute :weekday, :kind_of => [Integer, String], :default => "*"
attribute :command, :kind_of => String, :required => true
attribute :user, :kind_of => String, :default => "root"
attribute :mailto, :kind_of => [String, NilClass]
attribute :path, :kind_of => [String, NilClass]
attribute :home, :kind_of => [String, NilClass]
attribute :shell, :kind_of => [String, NilClass]

where

Providers

Now we need to create file know_host.rb in «providers» directory:

# Support whyrun
def whyrun_supported?
  true
end

use_inline_resources

action :create do
  key, comment = insure_for_file(new_resource)
  # Use a Ruby block to edit the file
  ruby_block "add #{new_resource.host} to #{new_resource.known_hosts_file}" do
    block do
      file = ::Chef::Util::FileEdit.new(new_resource.known_hosts_file)
      file.insert_line_if_no_match(/#{Regexp.escape(comment)}|#{Regexp.escape(key)}/, key)
      file.write_file
    end
  end
  new_resource.updated_by_last_action(true)
end

action :delete do
  key, comment = insure_for_file(new_resource)
  # Use a Ruby block to edit the file
  ruby_block "del #{new_resource.host} from #{new_resource.known_hosts_file}" do
    block do
      file = ::Chef::Util::FileEdit.new(new_resource.known_hosts_file)
      file.search_file_delete_line(/#{Regexp.escape(comment)}|#{Regexp.escape(key)}/)
      file.write_file
    end
  end
  new_resource.updated_by_last_action(true)
end

def insure_for_file(new_resource)
  key = (new_resource.key || `ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1`)
  comment = key.split("\n").first || ""

  Chef::Application.fatal! "Could not resolve #{new_resource.host}" if key =~ /getaddrinfo/

  # Ensure that the file exists and has minimal content (required by Chef::Util::FileEdit)
  file new_resource.known_hosts_file do
    action        :create
    backup        false
    content       '# This file must contain at least one line. This is that line.'
    only_if do
      !::File.exists?(new_resource.known_hosts_file) || ::File.new(new_resource.known_hosts_file).readlines.length == 0
    end
  end
  [key, comment]
end

DSL Methods

action

The action method is used to define the steps that will be taken for each of the possible actions defined by the lightweight resource. Each action must be defined in separate action blocks within the same file. The syntax for the action method is as follows:

action :action_name do
  if @current_resource.exists
    Chef::Log.info "#{ @new_resource } already exists - nothing to do."
  else
    resource "resource_name" do
      Chef::Log.info "#{ @new_resource } created."
    end
  end
  new_resource.updated_by_last_action(true)
end

where:

current_resource

The current_resource method is used to represent a resource as it exists on the node at the beginning of the chef-client run. In other words: what the resource is currently. The chef-client compares the resource as it exists on the node to the resource that is created during the chef-client run to determine what steps need to be taken to bring the resource into the desired state. This method is often used as an instance variable (@current_resource).

For example:

action :add do
  unless @current_resource.exists
    cmd = "#{appcmd} add app /site.name:\"#{@new_resource.app_name}\""
    cmd « " /path:\"#{@new_resource.path}\""
    cmd « " /applicationPool:\"#{@new_resource.application_pool}\"" if @new_resource.application_pool
    cmd « " /physicalPath:\"#{@new_resource.physical_path}\"" if @new_resource.physical_path
    Chef::Log.debug(cmd)
    shell_out!(cmd)
    Chef::Log.info("App created")
  else
    Chef::Log.debug("#{@new_resource} app already exists - nothing to do")
  end
end

where the unless conditional statement checks to make sure the resource doesn’t already exist on a node, and then runs a series of commands when it doesn’t. If the resource already exists, the log entry would be «Foo app already exists - nothing to do.»

load_current_resource

The load_current_resource method is used to find a resource on a node based on a collection of attributes. These attributes are defined in a lightweight resource and are loaded by the chef-client when processing a recipe during a chef-client run. This method will ask the chef-client to look on the node to see if a resource exists with specific matching attributes.

For example:

def load_current_resource
  @current_resource = Chef::Resource::TransmissionTorrentFile.new(@new_resource.name)
  Chef::Log.debug("#{@new_resource} torrent hash = #{torrent_hash}")
  path = "foo:#{@new_resource.att1}@#{@new_resource.att2}:#{@new_resource.att3}/path"
  @transmission = Opscode::Transmission::Client.new(path)
  @torrent = nil
  begin
    @torrent = @transmission.get_torrent(torrent_hash)
    info = "Found existing #{@new_resource} in swarm "+
    "with name of '#{@torrent.name}' and status of '#{@torrent.status_message}'"
    Chef::Log.info(info)
    @current_resource.torrent(@new_resource.torrent)
  rescue
    Chef::Log.debug("Cannot find #{@new_resource} in the swarm")
  end
  @current_resource
end

In the previous example, if a resource exists with matching attributes, the chef-client does nothing and if a resource does not exist with matching attributes, the chef-client will enforce the state declared in new_resource.

new_resource

The new_resource method is used to represent a resource as loaded by the chef-client during the chef-client run. In other words: what the resource should be. The chef-client compares the resource as it exists on the node to the resource that is created during the chef-client run to determine what steps need to be taken to bring the resource into the desired state.

For example:

action :delete do
  if exists?
    if ::File.writable?(new_resource.path)
      Chef::Log.info("Deleting #{new_resource} at #{new_resource.path}")
      ::File.delete(new_resource.path)
      new_resource.updated_by_last_action(true)
    else
      raise "Cannot delete #{new_resource} at #{new_resource.path}!"
    end
  end
end

where the chef-client checks to see if the file exists, then if the file is writable, and then attempts to delete the resource. path is an attribute of the new resource that is defined by the lightweight resource.

updated_by_last_action

The updated_by_last_action method is used to notify a lightweight resource that a node was updated successfully. For example, the cron_d lightweight resource in the cron cookbook:

action :create do
  t = template "/etc/cron.d/#{new_resource.name}" do
    cookbook new_resource.cookbook
    source "cron.d.erb"
    mode "0644"
    variables({
        :name => new_resource.name,
        :minute => new_resource.minute,
        :hour => new_resource.hour,
        :day => new_resource.day,
        :month => new_resource.month,
        :weekday => new_resource.weekday,
        :command => new_resource.command,
        :user => new_resource.user,
        :mailto => new_resource.mailto,
        :path => new_resource.path,
        :home => new_resource.home,
        :shell => new_resource.shell
      })
    action :create
  end
  t.run_action(:create)
  new_resource.updated_by_last_action(t.updated_by_last_action?)
end

where t.updated_by_last_action? uses a variable to check whether a new crontab entry was created. Also you should remember that t.updated_by_last_action? will work only with run_action, without it this method will return always false.

use_inline_resources

A lightweight resource should be set to inline compile mode by adding the use_inline_resources method at the top of the provider. This ensures that notifications work properly across the resource collection. The use_inline_resources method was added to the chef-client starting in version 11.0 to address the behavior described below. The use_inline_resources method should be considered a requirement for any lightweight resource authored against the 11.0+ versions of the chef-client. This behavior will become the default behavior in an upcoming version of the chef-client.

The reason why the use_inline_resources method exists at all is due to how the chef-client processes resources. Currently, the default behavior of the chef-client processes a single collection of resources, converged on the node in order.

A lightweight resource is often implemented using the core chef-client resources — file, template, package, and so on—as building blocks. A lightweight resource is then added to a recipe using the short name of the lightweight resource in the recipe (and not by using any of the building block resource components).

This situation can create problems with notifications because the chef-client includes embedded resources in the «single collection of resources» after the parent resource has been fully evaluated.

For example:

custom_resource "something" do
  action :run
  notifies :restart, "service[whatever]", :immediately
end

service "whatever" do
  action :nothing
end

If the custom_resource is built using the file resource, what happens during the chef-client run is:

custom_resource (not updated)
  file (updated)
service (skipped, due to ``:nothing``)

The custom_resource is converged completely, its state set to not updated before the file resource is evaluated. The notifies :restart is ignored and the service is not restarted.

If the author of the custom resource knows in advance what notification is required, then the file resource can be configured for the notification in the provider. For example:

action :run do
  file "/tmp/foo" do
    owner "root"
    group "root"
    mode "0644"
    notifies :restart, "service[whatever]", :immediately
  end
end

And then in the recipe:

service "whatever" do
  action :nothing
end

This approach works, but only when the author of the lightweight resource knows what should be notified in advance of the chef-client run. Consequently, this is less-than-ideal for most situations.

Using the use_inline_resources method will ensure that the chef-client processes a lightweight resource as if it were its own resource collection—a «mini chef-client run», effectively—that is converged before the chef-client finishes evaluating the parent lightweight resource. This ensures that any notifications that may exist in the embedded resources are processed as if they were notifications on the parent lightweight resource. For example:

custom_resource "something" do
  action :run
  notifies :restart, "service[whatever]", :immediately
end

service "whatever" do
  action :nothing
end

If the custom_resource is built using the file resource, what happens during the chef-client run is:

custom_resource (starts converging)
  file (updated)
custom_resource (updated, because ``file`` updated)
service (updates, because ``:immediately`` is set in the custom resource)

The use_inline_resources method should be considered a default method for any provider that defines a custom resource. It’s the correct behavior. And it will soon become the default behavior in a future version of the chef-client.

Because inline compile mode makes it impossible for embedded resources to notify resources in the parent resource collection, inline compile mode may cause issues with some provider implementations. In these cases, use a definition to work around inline compile mode.

whyrun_supported?

why-run mode is a way to see what the chef-client would have configured, had an actual chef-client run occurred. This approach is similar to the concept of «no-operation» (or «no-op»): decide what should be done, but then don’t actually do anything until it’s done right. This approach to configuration management can help identify where complexity exists in the system, where inter-dependencies may be located, and to verify that everything will be configured in the desired manner.

When why-run mode is enabled, a chef-client run will occur that does everything up to the point at which configuration would normally occur. This includes getting the configuration data, authenticating to the server, rebuilding the node object, expanding the run list, getting the necessary cookbook files, resetting node attributes, identifying the resources, and building the resource collection and does not include mapping each resource to a provider or configuring any part of the system.

When the chef-client is run in why-run mode, certain assumptions are made:

The whyrun_supported? method is used to set a lightweight provider to support why-run mode. The syntax for the whyrun_supported? method is as follows:

def whyrun_supported?
  true
end

where whyrun_supported? is set to true for any lightweight provider that supports using why-run mode. When why-run mode is supported by the a lightweight provider, the converge_by method is used to define the strings that are logged by the chef-client when it is run in why-run mode.

Log entries and rescue

Use the Chef::Log class in a lightweight provider to define log entries that are created during a chef-client run. The syntax for a log message is as follows:

Chef::Log.log_type("message")

where

For example, from the «repository.rb» provider in the yum cookbook:

action :add do
  unless ::File.exists?("/etc/yum.repos.d/#{new_resource.repo_name}.repo")
    Chef::Log.info "Adding #{new_resource.repo_name} repository to /etc/yum.repos.d/#{new_resource.repo_name}.repo"
    repo_config
  end
end

where the Chef::Log class appends .info as the log type. If the name of the repo was «foo», then the log message would be «Adding foo repository to /etc/yum.repos.d/foo.repo».

Another example shows two log entries, one that is triggered when a service is being restarted, and then another that is triggered after the service has been restarted:

action :restart do
  if @current_resource.running
    Chef::Log.debug "Restarting #{new_resource.service_name}"
    shell_out!(restart_command)
    new_resource.updated_by_last_action(true)
    Chef::Log.debug "Restarted #{new_resource.service_name}"
  end
end

Use the rescue clause to make sure that a log message is always provided. For example:

def load_current_resource
  ...
  begin
    ...
  rescue
    Chef::Log.debug("Cannot find #{@new_resource} in the swarm")
  end
  ...
end

Using LWRP

We finished with our LWRP. Let’s test it. Just add to «default.rb» you new LWRP, which will add to known hosts «github.com»:

# known hosts for github.com
my_cool_app_know_host 'github.com'

Upload cookbook on server and run chef-client on node:

$ knife cookbook upload my_cool_app
  Uploading my_cool_app    [0.1.0]
  Uploaded 1 cookbook.

$ vagrant provision chef_second_client
...
INFO: Found chef-client in /usr/bin/chef-client
INFO: runit_service[nginx] configured
INFO: ruby_block[add github.com to /etc/ssh/ssh_known_hosts] called
INFO: Chef Run complete in 11.920336698 seconds
INFO: Running report handlers
INFO: Report handlers complete

$ vagrant ssh chef_second_client

$ cat /etc/ssh/ssh_known_hosts
# This file must contain at least one line. This is that line.
# github.com SSH-2.0-OpenSSH_5.9p1 Debian-5ubuntu1+github5
|1|fvGKZG+jIkEntM5yBvzJ230TX1o=|9qP2wRdFIS+cAouirLYDb1Ibl7A= ssh-rsa...

Let’s check how it will delete this hosts:

my_cool_app_know_host 'github.com' do
  action :delete
end

Again upload on server and run chef-client:

$ knife cookbook upload my_cool_app
  Uploading my_cool_app    [0.1.0]
  Uploaded 1 cookbook.

$ vagrant provision chef_second_client
...
INFO: Found chef-client in /usr/bin/chef-client
INFO: runit_service[nginx] configured
INFO: ruby_block[del github.com from /etc/ssh/ssh_known_hosts] called
INFO: Chef Run complete in 17.109379291 seconds
INFO: Running report handlers
INFO: Report handlers complete

$ vagrant ssh chef_second_client

$ cat /etc/ssh/ssh_known_hosts
# This file must contain at least one line. This is that line.

As you can see, all works as expected.

HWRPs

When Chef first came out, there was no Lightweight Resources and Providers (LWRP) syntax and any hardcore extension to Chef had to be written in Ruby. However, Chef team saw a need to be filled and created LWRP, making it easier to create your own Resources. The problem comes when LWRP cannot fulfill all of your needs. This means you need to fall back to writing pure ruby code. For lack of a better term, I’ll call this method a HWRP, or Heavyweight Resources and Providers.

While writing a LWRP is meant to be simple and elegant, writing a HWRP is meant to be flexible. It gives you the full power of ruby in exchange for elegance.

HWRPs and LWRPS

With LWRP you are taught to create a Resource and a Provider together. This is the simplest way. However, just because you need to convert a resource definition or a provider into a HWRP you do not need to convert both.

The LWRP syntax «compiles» into real ruby code, so Chef will not know the difference in how they were defined. A valid cookbook directory structure:

libraries/
    provider_default.rb
providers/
resources/
    default.rb
recipes/
    default.rb
metadata.rb

Anything you put in «resources/» or «providers/» Chef will attempt to parse at runtime. We don’t want Chef trying to read our HWRP as the Chef DSL, we want it to interpret it as code. Luckily, anything stored in the «libraries/» folder Chef will try to import at runtime. A good example of this can be seen in the runit cookbook.

Example

Let’s go through an example. We are going to create a HWRP that is very simple, it already written as a LWRP. Our HWRP will called my_cool_app_known_host, to not conflict with already existed in cookbook LWRP my_cool_app_know_host.

Resource

First, we need to inherit from the appropriate Chef classes in our HWRP. Note the class hierarchy as well as the inheritance:

require 'chef/resource'

class Chef
  class Resource
    class MyCoolAppKnownHost < Chef::Resource

    # Some Magic Happens

    end
  end
end

Next, we should to override the initialize method to make sure we have some defaults. We aren’t defining all of our resource attributes here, just the ones that need defaults.

require 'chef/resource'

class Chef
  class Resource
    class MyCoolAppKnownHost < Chef::Resource

      def initialize(name, run_context=nil)
        super
        # Bind ourselves to the name with an underscore
        @resource_name = :my_cool_app_known_host
        # We need to tie to our provider
        @provider = Chef::Provider::MyCoolAppKnownHost
        # Default Action Goes here
        @action = :create
        @allowed_actions = [:create, :delete]

        # Now we need to set up any resource defaults
        @port = 22
        @known_hosts_file = '/etc/ssh/ssh_known_hosts'
        @host = name  # This is equivalent to setting :name_attribute => true
      end

    end
  end
end

Now lets set up some attribute methods in our HWRP. Make sure to read the code comments for an explanation of what is going on.

require 'chef/resource'

class Chef
  class Resource
    class MyCoolAppKnownHost < Chef::Resource

      def initialize(name, run_context=nil)
        super
        # Bind ourselves to the name with an underscore
        @resource_name = :my_cool_app_known_host
        # We need to tie to our provider
        @provider = Chef::Provider::MyCoolAppKnownHost
        # Default Action Goes here
        @action = :create
        @allowed_actions = [:create, :delete]

        # Now we need to set up any resource defaults
        @port = 22
        @known_hosts_file = '/etc/ssh/ssh_known_hosts'
        @host = name  # This is equivalent to setting :name_attribute => true
      end

      # Define the attributes we set defaults for
      def key(arg=nil)
        set_or_return(:key, arg, :kind_of => String)
      end

      def host(arg=nil)
        set_or_return(:host, arg, :kind_of => String)
      end

      def port(arg=nil)
        set_or_return(:port, arg, :kind_of => Integer)
      end

      def known_hosts_file(arg=nil)
        set_or_return(:known_hosts_file, arg, :kind_of => String)
      end

    end
  end
end

Providers

Very similar to resources, here is the basic class structure for a provider.

require 'chef/provider'

class Chef
  class Provider
    class MyCoolAppKnownHost < Chef::Provider

    # Magic Happens

    end
  end
end

While we don’t need to write an initialize method (we can), we do need to override load_current_resource.

require 'chef/provider'

class Chef
  class Provider
    class MyCoolAppKnownHost < Chef::Provider

      # We MUST override this method in our custom provider
      def load_current_resource
        # Here we keep the existing version of the resource
        # if none exists we create a new one from the resource we defined earlier
        @current_resource ||= Chef::Resource::MyCoolAppKnownHost.new(new_resource.name)

        # New resource represents the chef DSL block that is being run (from a recipe for example)
        @current_resource.key(new_resource.key)
        @current_resource.port(new_resource.port)
        @current_resource.known_hosts_file(new_resource.known_hosts_file)
        # Although you can reference @new_resource throughout the provider it is best to
        # only make modifications to the current version
        @current_resource.host(new_resource.host)
        @current_resource
      end

    end
  end
end

Now it is time to define what we do in our actions, with our HWRP we need to define methods like action_create to define a :create action. Chef will do some introspection to find these methods and hook them up.

require 'chef/provider'

class Chef
  class Provider
    class MyCoolAppKnownHost < Chef::Provider

      # We MUST override this method in our custom provider
      def load_current_resource
        # Here we keep the existing version of the resource
        # if none exists we create a new one from the resource we defined earlier
        @current_resource ||= Chef::Resource::MyCoolAppKnownHost.new(new_resource.name)

        # New resource represents the chef DSL block that is being run (from a recipe for example)
        @current_resource.key(new_resource.key)
        @current_resource.port(new_resource.port)
        @current_resource.known_hosts_file(new_resource.known_hosts_file)
        # Although you can reference @new_resource throughout the provider it is best to
        # only make modifications to the current version
        @current_resource.host(new_resource.host)
        @current_resource
      end

      def action_create
        Chef::Log.debug("#{@new_resource}: Create #{new_resource.host}")
      end

      def action_delete
        Chef::Log.debug("#{@new_resource}: Delete #{new_resource.host}")
      end

    end
  end
end

Now we can test it.

...
my_cool_app_known_host 'bitbucket.org'

To run chef-client in debug mode you should use -l attribute with settings debug. In vagrant you need set log_level for node in Vagrantfile:

chef_client.vm.provision :chef_client do |chef|

  chef.log_level = :debug

  ...
end

And see our HWRP execution in log:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.

$ vagrant provision chef_second_client
[chef_second_client] Running provisioner: chef_client...
Creating folder to hold client key...
Uploading chef client validation key...
...
DEBUG: my_cool_app_known_host[bitbucket.org]: Create bitbucket.org
DEBUG: Saving the current state of node second.example.com
INFO: Chef Run complete in 15.491575653 seconds
...

Now let’s add code for actions :create and :delete (this code similar to LWRP code):

require 'chef/provider'

class Chef
  class Provider
    class MyCoolAppKnownHost < Chef::Provider

      ...

      def action_create
        Chef::Log.debug("#{@new_resource}: Create #{new_resource.host}")

        key, comment = insure_for_file(new_resource)
        # Use a Ruby block to edit the file
        ruby_block "add #{new_resource.host} to #{new_resource.known_hosts_file}" do
          block do
            file = ::Chef::Util::FileEdit.new(new_resource.known_hosts_file)
            file.insert_line_if_no_match(/#{Regexp.escape(comment)}|#{Regexp.escape(key)}/, key)
            file.write_file
          end
        end
        new_resource.updated_by_last_action(true)
      end

      def action_delete
        Chef::Log.debug("#{@new_resource}: Delete #{new_resource.host}")

        key, comment = insure_for_file(new_resource)
        # Use a Ruby block to edit the file
        ruby_block "del #{new_resource.host} from #{new_resource.known_hosts_file}" do
          block do
            file = ::Chef::Util::FileEdit.new(new_resource.known_hosts_file)
            file.search_file_delete_line(/#{Regexp.escape(comment)}|#{Regexp.escape(key)}/)
            file.write_file
          end
        end
        new_resource.updated_by_last_action(true)
      end

      private

      def insure_for_file(new_resource)
        key = (new_resource.key || `ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1`)
        comment = key.split("\n").first || ""

        Chef::Application.fatal! "Could not resolve #{new_resource.host}" if key =~ /getaddrinfo/

        # Ensure that the file exists and has minimal content (required by Chef::Util::FileEdit)
        file new_resource.known_hosts_file do
          action        :create
          backup        false
          content       '# This file must contain at least one line. This is that line.'
          only_if do
            !::File.exists?(new_resource.known_hosts_file) || ::File.new(new_resource.known_hosts_file).readlines.length == 0
          end
        end
        [key, comment]
      end

    end
  end
end

And check how it works:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.

$ vagrant provision chef_second_client
[chef_second_client] Running provisioner: chef_client...
Creating folder to hold client key...
Uploading chef client validation key...
...
WARN: Cloning resource attributes for file[/etc/ssh/ssh_known_hosts] from prior resource (CHEF-3694)
WARN: Previous file[/etc/ssh/ssh_known_hosts]: /var/chef/cache/cookbooks/my_cool_app/providers/know_host.rb:39:in `insure_for_file'
WARN: Current  file[/etc/ssh/ssh_known_hosts]: /var/chef/cache/cookbooks/my_cool_app/libraries/provider_known_host.rb:62:in `insure_for_file'
INFO: ruby_block[add bitbucket.org to /etc/ssh/ssh_known_hosts] called
...

$ vagrant ssh chef_second_client

vagrant@precise64:~$ cat /etc/ssh/ssh_known_hosts
# This file must contain at least one line. This is that line.
# github.com SSH-2.0-OpenSSH_6.2p2 Ubuntu-6ubuntu0.1+github2
|1|Daaa5BVIzI52zRmJ2ifMfOkkLJE=|Y611mqhJbVkdJ1onVkaqfV+3iks= ...
# bitbucket.org SSH-2.0-OpenSSH_5.3
|1|jycCFcglLajRKYIlyAJaD+zmOjw=|51hFV+x1XIZNdiMGG6K0Xz+Nkds= ...

As a result, we wrote HWRP, which performs the same job as LWRP. HWRP is not so simple and elegant as LWRP, but have huge flexibility, because have more control by Ruby language.

Definitions

A definition is used to declare resources so they can be added to the resource collection. A definition is not a resource or a lightweight resource. A definition does not have an associated provider. A definition groups two (or more) resource declarations. There is no limit to the number of resources that can be part of a definition. All definitions within a cookbook must be located in the «definitions/» folder. A definition is never declared into a cookbook. A definition is best-used when:

In our my_cool_app cookbook we are using nginx_site definition from nginx cookbook.

Right now definitions continue working in Chef, but better to use LWRP (or HWRP) instead definition (in chapter «[sec:testing-foodcritic] » you will see warning about this).

Example

Let’s create definition in our my_cool_app cookbook. We will move our nginx conf creation into definition. Our code:

# create nginx config from temlate nginx.conf.erb
nginx_config = "#{node['nginx']['dir']}/sites-available/#{node['my_cool_app']['name']}.conf"
template nginx_config do
  source "nginx.conf.erb"
  mode "0644"
end

# activate conf in nginx
nginx_site "#{node['my_cool_app']['name']}.conf"

we moved to definition enable_web_site:

define :enable_web_site, :enable => true, :template => "site.conf.erb" do
  if params[:enable]
    # create nginx config from temlate nginx.conf.erb
    nginx_config = "#{node['nginx']['dir']}/sites-available/#{params[:name]}.conf"
    template nginx_config do
      source params[:template]
      mode "0644"
    end
    # activate conf in nginx
    nginx_site "#{params[:name]}.conf"
  else
    # deactivateconf in nginx
    nginx_site "#{params[:name]}.conf" do
      enable    false
    end
  end
end

And now we can replace code in [lst:cookbook-definitions-default1] by call of enable_web_site:

# enable website
enable_web_site node['my_cool_app']['name'] do
  template "nginx.conf.erb"
end

Now it remains to check how it works:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.

$ vagrant provision chef_second_client
...
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] created file /etc/nginx/sites-available/my_cool_app.conf
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] updated file contents /etc/nginx/sites-available/my_cool_app.conf
INFO: template[/etc/nginx/sites-available/my_cool_app.conf] mode changed to 644
INFO: execute[nxensite my_cool_app.conf] ran successfully
...

As you can see our definition works as expected.

Ohai

Ohai detects data about your operating system. It can be used standalone, but its primary purpose is to provide node data to Chef.

When invoked, it collects detailed, extensible information about the machine it’s running on, including Chef configuration, hostname, FQDN, networking, memory, CPU, platform, and kernel data.

When Chef configures the node object during each Chef run, these attributes are used by the chef-client to ensure that certain properties remain unchanged. These properties are also referred to as automatic attributes (which, as you remember, impossible to override by attributes from cookbooks, environments, roles and nodes). For example:

node['platform'] # The platform on which a node is running. This attribute helps determine which providers will be used.
node['platform_version']  # The version of the platform. This attribute helps determine which providers will be used.
node['hostname']  # The host name for the node.

Example

Let’s create new recipe, which will use Ohai attributes and create our own Ohai attributes.

Recipe node

First of all, we will create new recipe «node», which will install Node.js on nodes in our my_cool_app cookbook.

New default attributes:

default['my_cool_app']['nodejs']['version'] = '0.10.26'
default['my_cool_app']['nodejs']['checksum'] = '2340ec2dce1794f1ca1c685b56840dd515a271b2'
default['my_cool_app']['nodejs']['dir'] = '/usr/local'
default['my_cool_app']['nodejs']['src_url'] = "http://nodejs.org/dist"

And recipe:

include_recipe "build-essential"

case node['platform_family']
  when 'rhel','fedora'
    package "openssl-devel"
  when 'debian'
    package "libssl-dev"
end

nodejs_tar = "node-v#{node['my_cool_app']['nodejs']['version']}.tar.gz"
nodejs_tar_path = nodejs_tar
if node['my_cool_app']['nodejs']['version'].split('.')[1].to_i >= 5
  nodejs_tar_path = "v#{node['my_cool_app']['nodejs']['version']}/#{nodejs_tar_path}"
end
# Let the user override the source url in the attributes
nodejs_src_url = "#{node['my_cool_app']['nodejs']['src_url']}/#{nodejs_tar_path}"

remote_file "/usr/local/src/#{nodejs_tar}" do
  source nodejs_src_url
  checksum node['my_cool_app']['nodejs']['checksum']
  mode 0644
  action :create_if_missing
end

# --no-same-owner required overcome "Cannot change ownership" bug
# on NFS-mounted filesystem
execute "tar --no-same-owner -zxf #{nodejs_tar}" do
  cwd "/usr/local/src"
  creates "/usr/local/src/node-v#{node['my_cool_app']['nodejs']['version']}"
end

bash "compile node.js" do
  cwd "/usr/local/src/node-v#{node['my_cool_app']['nodejs']['version']}"
  code «-EOH
    PATH="/usr/local/bin:$PATH"
    ./configure --prefix=#{node['my_cool_app']['nodejs']['dir']} && \
    make
  EOH
  creates "/usr/local/src/node-v#{node['my_cool_app']['nodejs']['version']}/node"
end

execute "nodejs make install" do
  environment({"PATH" => "/usr/local/bin:/usr/bin:/bin:$PATH"})
  command "make install"
  cwd "/usr/local/src/node-v#{node['my_cool_app']['nodejs']['version']}"
  not_if do
    File.exists?("#{node['my_cool_app']['nodejs']['dir']}/bin/node") &&
    `#{node['my_cool_app']['nodejs']['dir']}/bin/node --version`.chomp == "v#{node['my_cool_app']['nodejs']['version']}"
  end
end

As you can see, we use node['platform_family'] Ohai variable, which help for us understand type of OS on node and install needed package.

Also we used cookbook build-essential, because we will install node.js from source. In this case we should add it as dependency in our metadata.rb file:

depends 'nginx',           '~> 2.2.0'
depends 'build-essential'

To use this recipe we should add it in run_list:

{
  "run_list": [
    "recipe[my_cool_app]",
    "recipe[my_cool_app::node]"
  ]
}

But I want run this recipe with default recipe, so I will leave run_list as is and add node recipe in default recipe:

...
include_recipe 'my_cool_app::node'

Now it remains to check how it works:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.

$ vagrant provision chef_second_client
INFO: remote_file[/usr/local/src/node-v0.10.26.tar.gz] created file /usr/local/src/node-v0.10.26.tar.gz
INFO: remote_file[/usr/local/src/node-v0.10.26.tar.gz] updated file contents /usr/local/src/node-v0.10.26.tar.gz
INFO: remote_file[/usr/local/src/node-v0.10.26.tar.gz] mode changed to 644
INFO: execute[tar --no-same-owner -zxf node-v0.10.26.tar.gz] ran successfully
INFO: bash[compile node.js] ran successfully
INFO: execute[nodejs make install] ran successfully

$ vagrant ssh chef_second_client

vagrant@precise64:~$ node -v
v0.10.26

As you can see node recipe installed Node.js on our node.

Ohai plugin

In our node recipe we used maybe not best way to check node version, which already installed on node (if version mismatch - we should install needed). Let’s create Ohai plugin, which will give for use node.js version from server. First of all create in my_cool_app new recipe ohai_plugin.rb with content:

template "#{node['ohai']['plugin_path']}/system_node_js.rb" do
  source "plugins/system_node_js.rb.erb"
  owner "root"
  group "root"
  mode 00755
  variables(
    :node_js_bin => "#{node['my_cool_app']['nodejs']['dir']}/bin/node"
  )
end

include_recipe "ohai"

This recipe will generate ohai plugin from system_node_js.rb.erb template. Next we should create this template in folder «templates/default/plugins»:

provides "system_node_js"
provides "system_node_js/version"

system_node_js Mash.new unless system_node_js
system_node_js[:version] = nil unless system_node_js[:version]

status, stdout, stderr = run_command(:no_status_check => true, :command => "<%= @node_js_bin %> --version")

system_node_js[:version] = stdout[1..-1] if 0 == status

In first two lines we set by method provides automatic attributes, which will provide for us this plugin. Most of the information we want to lookup would be nested in some way, and ohai tends to do this by storing the data in a Mash (similar to Ruby hash type). This can be done by creating a new mash and setting the attribute to it. We did this with system_node_js. In the end of code, plugin set the version of node.js, if node.js installed on node. That’s it!

Also we should add new dependency for our cookbook - ohai cookbook:

depends 'nginx',           '~> 2.2.0'
depends 'build-essential'
depends 'ohai'

Next, let’s try this plugin by adding node.rb recipe this content:

include_recipe "build-essential"

include_recipe "my_cool_app::ohai_plugin"
Chef::Log.info "Installed Node version: #{node['system_node_js']['version']}" if node['system_node_js']

case node['platform_family']
  when 'rhel','fedora'
    package "openssl-devel"
  when 'debian'
    package "libssl-dev"
end

In this case we can little change our node.js recipe:

execute "nodejs make install" do
  environment({"PATH" => "/usr/local/bin:/usr/bin:/bin:$PATH"})
  command "make install"
  cwd "/usr/local/src/node-v#{node['my_cool_app']['nodejs']['version']}"
  not_if {node['system_node_js'] && node['system_node_js']['version'] == node['my_cool_app']['nodejs']['version'] }
end

Now we can check how it works:

$ knife cookbook upload my_cool_app
Uploading my_cool_app    [0.1.0]
Uploaded 1 cookbook.

$ vagrant provision chef_second_client
...
INFO: template[/etc/chef/ohai_plugins/system_node_js.rb] created file /etc/chef/ohai_plugins/system_node_js.rb
INFO: template[/etc/chef/ohai_plugins/system_node_js.rb] updated file contents /etc/chef/ohai_plugins/system_node_js.rb
INFO: template[/etc/chef/ohai_plugins/system_node_js.rb] owner changed to 0
INFO: template[/etc/chef/ohai_plugins/system_node_js.rb] group changed to 0
INFO: template[/etc/chef/ohai_plugins/system_node_js.rb] mode changed to 755
...
$ vagrant provision chef_second_client
...
WARN: Current  service[nginx]: /var/chef/cache/cookbooks/nginx/recipes/source.rb:123:in `from_file'
INFO: Installed Node version: 0.10.26
WARN: Cloning resource attributes for package[libssl-dev] from prior resource (CHEF-3694)
...

As you can see, after first provision (knife cook, knife ssh 'sudo chef-client') execution Ohai plugin will be installed, but not executed. Only on second execution chef-client will use Ohai plugin (because Ohai plugins load before running run_list).

Ohai 7

Ohai 6 had served us well. However, it had an important architectural limitation that prevented us from implementing some cool ideas, such as differentiating collected data as critical or optional. This limitation was because Ohai 6 treated plugins as monolithic blocks of code.

Ohai 7 introduces a new DSL, which makes it easier to write custom plugins, provides better code organization, and sets us up for the future. Here is what an Ohai 7 plugin looks like:

Ohai.plugin(:Name) do
  provides "attribute", "attribute/subattribute"
  depends "kernel", "users"

  def a_shared_method
    # some Ruby code that defines the shared method
    attribute Mash.new
  end

  collect_data(:default) do
    # some Ruby code
    attribute Mash.new
  end

  collect_data(:windows) do
    # some Ruby code that gets run only on Windows
    attribute Mash.new
  end

end

Two important pieces of the new DSL are:

Read more about the new DSL here.

To migrate our plugin on Ohai 7 is very simple:

Ohai.plugin(:SystemNodeJs) do
  provides 'system_node_js'
  provides 'system_node_js/version'

  collect_data do
    system_node_js Mash.new
    system_node_js[:version] = nil unless system_node_js[:version]
    status, stdout, stderr = run_command(:no_status_check => true, :command => "<%= @node_js_bin %> --version")
    system_node_js[:version] = stdout[1..-1] if 0 == status
  end
end

Ohai 7 is backwards compatible with existing Ohai 6 plugins. But none of the new (or future) functionality will be available to version 6 plugins. All of your existing plugins will continue to work with Ohai 7.

Summary

A cookbook is the fundamental unit of configuration and policy distribution. Knowledge of how to write cookbooks is very important to fully use all power of Chef.

Testing Cookbooks

Knowing how to write good cookbooks insufficient if its will not be covered by tests. Like in any good software products, cookbook tests avoid bugs, mistakes in code, which is very important. In this chapter we look at the tools that help us test Chef cookbooks.

Test Types

Exists several types of software testing. Let’s look at some of them.

Unit Testing

Unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use. Ideally, each test case is independent from the others. Substitutes such as method stubs, mock objects, fakes, and test harnesses can be used to assist testing a module in isolation. Unit tests are typically written and run by software developers to ensure that code meets its design and behaves as intended.

Integration Testing

Integration testing is the phase in software testing in which individual software modules are combined and tested as a group. It occurs after unit testing and before validation testing. Integration testing takes as its input modules that have been unit tested, groups them in larger aggregates, applies tests defined in an integration test plan to those aggregates, and delivers as its output the integrated system ready for system testing.

Acceptance Testing

Acceptance testing, a testing technique performed to determine whether or not the software system has met the requirement specifications. The main purpose of this test is to evaluate the system’s compliance with the business requirements and verify if it is has met the required criteria for delivery to end users.

ChefSpec

ChefSpec is a unit testing framework for testing Chef cookbooks. ChefSpec makes it easy to write examples and get fast feedback on cookbook changes without the need for virtual machines or cloud servers. ChefSpec using RSpec for writing tests, what is why you should know Rspec at least basic stuff.

Installing

Let’s cover our cookbook by chefspec tests. First we should add this gem in Gemfile:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'
gem 'thor-foodcritic'
gem 'chefspec'

And you should to execute bundle command to install this gem.

Testing

Next we should create config for chefspec:

require 'chefspec'
require 'chefspec/berkshelf' # if you are using librarian, when you should require 'chefspec/librarian'

RSpec.configure do |config|
  # Specify the Chef log_level (default: :warn)
  config.log_level = :warn

  # Specify the operating platform to mock Ohai data from (default: nil)
  config.platform = 'ubuntu'

  # Specify the operating version to mock Ohai data from (default: nil)
  config.version = '12.04'
end

And add some unit tests for default recipe:

require 'spec_helper'

describe 'my_cool_app::default' do
  let(:chef_run) { ChefSpec::Runner.new.converge(described_recipe) }

  %w(git ntp).each do |pack|
    it "install #{pack} package" do
      expect(chef_run).to install_package(pack)
    end
  end

  it 'create directory for web app' do
    expect(chef_run).to create_directory('/var/www/my_cool_app').with(
      user:   'vagrant',
      mode:  "0755"
    )
  end

  it 'create web app nginx config' do
    expect(chef_run).to create_template('/etc/nginx/sites-available/my_cool_app.conf')
  end

  it 'enable nginx service' do
    expect(chef_run).to enable_service('nginx')
  end

  it 'include node recipe' do
    expect(chef_run).to include_recipe('my_cool_app::node')
  end

end

Now we can check our tests:

$ rspec

FFFFFF

Finished in 2.25 seconds
6 examples, 0 failures

Our tests failed, because we used nginx_site definition, but dont have nginx service inside our recipe. We can fix this by adding nginx default recipe inside our default recipe (nginx default recipe contain service resource).

...

service 'nginx' do
  supports :status => true, :restart => true, :reload => true
  action   :start
end

You can ask me: «Wait, we already have role nginx inside our node, which install nginx. Does in this case we will call nginx recipe twice?». Yes, your are right. But as you remember, main idea of Chef is idempotence, so on second call of nginx recipe it will do nothing (if recipe written in this way). So we shouldn’t worry about this:

...

# nginx recipe
include_recipe 'nginx'

# enable website
enable_web_site node['my_cool_app']['name'] do
  template "nginx.conf.erb"
end

...

Add test for include_recipe call and check tests again:

$ rspec

.......

Finished in 2.25 seconds
7 examples, 0 failures

As you can see all tests pass.

Now let’s cover our node recipe:

require 'spec_helper'

describe 'my_cool_app::node' do
  let(:platform) { 'ubuntu' }
  let(:platform_version) { '12.04' }
  let(:chef_run) { ChefSpec::Runner.new(platform: platform, version: platform_version).converge(described_recipe) }
  let(:nodejs_version) { '0.10.26' }
  let(:nodejs_tar) { "node-v#{nodejs_version}.tar.gz" }

  it "install libssl-dev package" do
    expect(chef_run).to install_package('libssl-dev')
  end

  context 'rhel or fedora' do
    let(:platform) { 'redhat' }
    let(:platform_version) { '6.5' }

    it "install openssl-devel package" do
      expect(chef_run).to install_package('openssl-devel')
    end
  end

  it 'download node if missing' do
    expect(chef_run).to create_remote_file_if_missing("/usr/local/src/#{nodejs_tar}")
  end

  it 'unpack node' do
    expect(chef_run).to run_execute("tar --no-same-owner -zxf #{nodejs_tar}")
  end

  it 'install node' do
    expect(chef_run).to run_execute('make install').with(cwd: "/usr/local/src/node-v#{nodejs_version}")
  end
end

As you can see, you can pass platform and version, which will be used as Ohai attribute values. This used to check, what depend on platform recipe will install different dependencies. Our results:

$ rspec

..........

Finished in 3.19 seconds
12 examples, 0 failures

As a result, we covered cookbook by unit tests using chefspec.

Fauxhai

Ohai is a tool that is used to detect attributes on a node, and then provide these attributes to the chef-client at the start of every chef-client run. Ohai is required by the chef-client and must be present on a node. It’s awesome, but this can be problem for testing. What is why exist Fauxhai. Fauxhai is a gem for mocking out ohai data in your chef testing.

Testing

As you can see from our node specs, we can set platform and version, but we can stub additional Ohai attribute by using Fauxhai. Let’s look at example:

require 'chefspec'

describe 'my_cool_app::default' do
  [1, 2, 4].each do |kernels|
    context "on Ubuntu with #{kernels} kernels" do
      let(:chef_run) { ChefSpec::ChefRunner.new.converge(described_recipe) }

      before do
        Fauxhai.mock(platform: 'ubuntu', version: '12.04', fqdn: 'example.com', cpu: { 'real' => kernels, 'total' => kernels })
      end

      it 'install htop' do
        expect(chef_run).to install_package('htop')
      end

      it 'create file /tmp/cpu_count' do
        expect(chef_run).to render_file('/tmp/cpu_count').with_content(kernels.to_s)
      end
    end
  end
end

As you can see from example, we mock platform, fqdn and cpu numbers from Ohai attributes. And in our recipe we create /tmp/cpu_count file, which contain number of cpu on node. By tests we check what this works on different values of Ohai attributes.

Also we can set node attributes by ChefSpec::Runner:

...

  context 'node versions' do
    let(:system_node_version) { nil }
    let(:chef_run) do
      ChefSpec::Runner.new(platform: platform, version: platform_version) do |node|
        node.automatic['system_node_js'] = { 'version' => system_node_version } if system_node_version
      end.converge(described_recipe)
    end

    it 'install node if version not specified' do
      expect(chef_run).to run_execute('make install').with(cwd: "/usr/local/src/node-v#{nodejs_version}")
    end

    context 'installed different version' do
      let(:system_node_version) { '0.8.0' }

      it 'install node if version is not the same' do
        expect(chef_run).to run_execute('make install').with(cwd: "/usr/local/src/node-v#{nodejs_version}")
      end
    end

    context 'installed same version' do
      let(:system_node_version) { nodejs_version }

      it 'do not install node' do
        expect(chef_run).not_to run_execute('make install').with(cwd: "/usr/local/src/node-v#{nodejs_version}")
      end
    end
  end

...

And check what this new tests is pass:

$ rspec

...............

Finished in 3.76 seconds
15 examples, 0 failures

We covered different installation (or not installation) of node.js on node.

Test Kitchen

Test Kitchen is a test harness tool to execute your configured code on one or more platforms in isolation. A driver plugin architecture is used which lets you run your code on various cloud providers and virtualization technologies such as Amazon EC2, Blue Box, CloudStack, Digital Ocean, Rackspace, OpenStack, Vagrant, Docker, LXC containers, and more. Many testing frameworks are already supported out of the box including Bats, shUnit2, RSpec, Serverspec, etc.

Installing

Let’s cover our cookbook by test kitchen. First we should add this gem in Gemfile:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'
gem 'thor-foodcritic'
gem 'chefspec'
gem 'test-kitchen'
gem 'kitchen-vagrant'

And you should to execute bundle command to install this gems. We can check what kitchen installed:

$ kitchen version
Test Kitchen version 1.2.1
$ kitchen help
Commands:
  kitchen console                         # Kitchen Console!
  kitchen converge [INSTANCE|REGEXP|all]  # Converge one or more instances
  kitchen create [INSTANCE|REGEXP|all]    # Create one or more instances
  kitchen destroy [INSTANCE|REGEXP|all]   # Destroy one or more instances
  kitchen diagnose [INSTANCE|REGEXP|all]  # Show computed diagnostic configuration
  kitchen driver                          # Driver subcommands
  kitchen driver create [NAME]            # Create a new Kitchen Driver gem project
  kitchen driver discover                 # Discover Test Kitchen drivers published on RubyGems
  kitchen driver help [COMMAND]           # Describe subcommands or one specific subcommand
  kitchen help [COMMAND]                  # Describe available commands or one specific command
  kitchen init                            # Adds some configuration to your cookbook so Kitchen can rock
  kitchen list [INSTANCE|REGEXP|all]      # Lists one or more instances
  kitchen login INSTANCE|REGEXP           # Log in to one instance
  kitchen setup [INSTANCE|REGEXP|all]     # Setup one or more instances
  kitchen test [INSTANCE|REGEXP|all]      # Test one or more instances
  kitchen verify [INSTANCE|REGEXP|all]    # Verify one or more instances
  kitchen version                         # Print Kitchen's version information

Now we’ll add Test Kitchen to our project by using the init subcommand:

$ kitchen init
    create  .kitchen.yml
    append  Thorfile
    create  test/integration/default

What’s going on here? The kitchen init subcommand will create an initial configuration file for Test Kitchen called .kitchen.yml. A few directories were created but these are only a convenience – you don’t strictly need «test/integration/default» in your project. You can see that you have a .gitignore file in your project’s root which will tell Git to never commit a directory called .kitchen and something called .kitchen.local.yml. Finally, a gem called kitchen-vagrant was installed. By itself Test Kitchen can’t do very much. It needs one or more Drivers which are responsible for managing the virtual machines we need for testing. At present there are many different Test Kitchen Drivers but we’re going to stick with the Kitchen Vagrant Driver for now.

Let’s turn our attention to the .kitchen.yml file. While Test Kitchen may have created the initial file automatically, it’s expected that you will read and edit this file. Opening this file in your editor of choice we see something like the following:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:

Very briefly we can cover the 4 main sections you’re likely to find in a .kitchen.yml file:

Let’s say for argument’s sake that we only care about running our Chef cookbook on Ubuntu 12.04 distributions. In that case, we can edit the .kitchen.yml file so that the list of platforms has only one entry like so:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:

To see the results of our work, let’s run the kitchen list subcommand:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>

So what’s this «default-ubuntu-1204» thing and what’s an «Instance»? A Test Kitchen Instance is a pairwise combination of a Suite and a Platform as laid out in your .kitchen.yml file. Test Kitchen has auto-named your only instance by combining the Suite name («default») and the Platform name («ubuntu-12.04») into a form that is safe for DNS and hostname records, namely «default-ubuntu-1204».

Okay, let’s spin this Instance up to see what happens. Test Kitchen calls this the Create Action. We’re going to be painfully explicit and ask Test Kitchen to only create the «default-ubuntu-1204» instance:

$ kitchen create default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Creating <default-ubuntu-1204>...
       Bringing machine 'default' up with 'virtualbox' provider...
       [default] Importing base box 'opscode-ubuntu-12.04'...
       [default] Matching MAC address for NAT networking...
       [default] Setting the name of the VM...
       [default] Clearing any previously set forwarded ports...
       [default] Clearing any previously set network interfaces...
       [default] Preparing network interfaces based on configuration...
       [default] Forwarding ports...
       [default] -- 22 => 2222 (adapter 1)
       [default] Running 'pre-boot' VM customizations...
       [default] Booting VM...
       [default] Waiting for machine to boot. This may take a few minutes...
       [default] Machine booted and ready!
       [default] Setting hostname...
       Vagrant instance <default-ubuntu-1204> created.
       Finished creating <default-ubuntu-1204> (3m56.11s).
-----> Kitchen is finished. (3m57.66s)

If you are a Vagrant user then the line containing vagrant up --no-provision will look familiar. Let’s check the status of our instance now:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     Created

Running Kitchen Converge

Now let’s let Test Kitchen run it for us on our Ubuntu 12.04 instance:

$ kitchen converge default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Converging <default-ubuntu-1204>...
       Preparing files for transfer
       Resolving cookbook dependencies with Berkshelf 2.0.14...
       Removing non-cookbook files before transfer
       Transfering files to <default-ubuntu-1204>
       ...
       Finished converging <default-ubuntu-1204> (10m24.13s).
-----> Kitchen is finished. (10m26.31s)

To quote our Chef run, that was too easy. If you are a Chef user then part of the output above should look familiar to you. Here’s what happened at a high level:

There’s nothing to stop you from running this command again (or over-and-over for that matter) so, let’s see what happens:

$ kitchen converge default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Converging <default-ubuntu-1204>...
       Preparing files for transfer
       Resolving cookbook dependencies with Berkshelf 2.0.14...
       Removing non-cookbook files before transfer
       Transfering files to <default-ubuntu-1204>
       ...
       Finished converging <default-ubuntu-1204> (0m13.13s).
-----> Kitchen is finished. (0m15.23s)

That ran a lot faster didn’t it? Here’s what happened this time:

Let’s check the status of our instance:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     Converged

A clean converge run, success!

Manually Verifying

Test Kitchen has a login subcommand for just these kinds of situations:

$ kitchen login default-ubuntu-1204
Welcome to Ubuntu 12.04.3 LTS (GNU/Linux 3.8.0-29-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
Last login: Thu Mar  6 22:01:08 2014 from 10.0.2.2
vagrant@default-ubuntu-1204:~$ ps ax | grep nginx
 6151 ?        Ss     0:00 nginx: master process /usr/sbin/nginx
26282 ?        S      0:00 nginx: worker process
26758 pts/0    S+     0:00 grep --color=auto nginx

As you can see by the prompt above we are now in the «default-ubuntu-1204» instance and nginx installed inside it.

Writing a Test

Now if you are interested in writing some tests, Test Kitchen has several options available to you. The component that helps facilitate testing on your instances is called Busser. Just like Test Kitchen it is a RubyGem library and it provides a plugin system so that you can wire in whatever testing framework your heart desires. A quick search on the RubyGems website returns several testing frameworks currently available to you.

To keep things simple we’re going to use the busser-bats runner plugin which uses the Bash Automated Testing System also known as bats.

We need to put our test files in a specific location, so let’s create the directory:

$ mkdir -p test/integration/default/bats

It looks long and dense, but each directory has some meaning to Test Kitchen and the Busser helper:

Let’s write a test. Create a new file called test/integration/default/bats/nginx_installed.bats with the following:

#!/usr/bin/env bats

@test "nginx binary is found in PATH" {
  run which nginx
  [ "$status" -eq 0 ]
}

Now to put our test to the test. For this we’ll use the verify subcommand:

$ kitchen verify default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Setting up <default-ubuntu-1204>...
Fetching: thor-0.18.1.gem (100%)
Fetching: busser-0.6.0.gem (100%)
...
Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
-----> Running bats test suite
  nginx binary is found in PATH

       1 test, 0 failures
       Finished verifying <default-ubuntu-1204> (0m1.39s).
-----> Kitchen is finished. (0m16.51s)

All right!

A few things of note from the output above:

Let’s check the status of our instance again:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     Verified

Let’s add more tests:

@test "nginx config is valid" {
  run sudo nginx -t
  [ "$status" -eq 0 ]
}

@test "nginx is running" {
  run service nginx status
  [ "$status" -eq 0 ]
  [ "$output" == " * nginx is running" ]
}

Running Kitchen Test

Now it’s time to introduce to the test meta-action which helps you automate all the previous actions so far into one command. Recall that we currently have our instance in a «verified» state. With this in mind, let’s run kitchen test:

$ kitchen test default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Cleaning up any prior instances of <default-ubuntu-1204>
-----> Destroying <default-ubuntu-1204>...
       Finished destroying <default-ubuntu-1204> (0m0.00s).
-----> Testing <default-ubuntu-1204>
...
Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
-----> Running bats test suite
  nginx binary is found in PATH

       1 test, 0 failures
       Finished verifying <default-ubuntu-1204> (0m1.39s).
-----> Kitchen is finished. (0m16.51s)

There’s only one remaining action left that needs a mention: the Destroy Action which destroys the instance. With this in mind, here’s what Test Kitchen is doing in the Test Action:

A few details with regards to test:

Finally, let’s check the status of the instance:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>

Adding a Platform

Now that we are masters of the Ubuntu platform, let’s add support for CentOS to our cookbook. This shouldn’t be too bad. Open .kitchen.yml in your editor and the centos-6.4 line to your platforms list so that it resembles:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:

Now let’s check the status of our instances:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  Chef Solo    <Not Created>
default-centos-64    Vagrant  Chef Solo    <Not Created>

We’re going to use two shortcuts here in the next command:

Let’s see how CentOS runs our cookbook:

$ kitchen verify 64
-----> Starting Kitchen (v1.2.1)
-----> Creating <default-centos-64>...
...
-----> Running bats test suite
   nginx binary is found in PATH
   nginx config is valid                                                    2/3
   nginx config is valid
   nginx is running                                                         3/3
   nginx is running
          (in test file /tmp/busser/suites/bats/nginx_installed.bats, line 16)

       3 tests, 1 failure

We should fix failed test:

@test "nginx is running" {
   run service nginx status
   [ "$status" -eq 0 ]
-  [ "$output" == " * nginx is running" ]
+  [ $(expr "$output" : ".*nginx.*running") -ne 0 ]
}

And check again on all instances:

$ kitchen verify
-----> Starting Kitchen (v1.2.1)
-----> Creating <default-centos-64>...
...
-----> Verifying <default-ubuntu-1204>...
       Suite path directory /tmp/busser/suites does not exist, skipping.
Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
-----> Running bats test suite
   nginx binary is found in PATH
   nginx config is valid
   nginx is running

3 tests, 0 failures
       Finished verifying <default-ubuntu-1204> (0m1.46s).
-----> Verifying <default-centos-64>...
       Removing /tmp/busser/suites/bats
       Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
-----> Running bats test suite
   nginx binary is found in PATH
   nginx config is valid                                                    2/3
   nginx config is valid
   nginx is running                                                         3/3
   nginx is running

       3 tests, 0 failures
       Finished verifying <default-centos-64> (0m1.81s).

Nice! We’ve verified that our cookbook works on Ubuntu 12.04 and CentOS 6.4. Since the CentOS instance will hang out for no good reason, let’s kill it for now:

$ kitchen destroy
-----> Starting Kitchen (v1.2.1)
-----> Destroying <default-ubuntu-1204>...
       ...
       Finished destroying <default-ubuntu-1204> (0m5.82s).
-----> Destroying <default-centos-64>...
       ...
       Finished destroying <default-centos-64> (0m5.41s).
-----> Kitchen is finished. (0m12.95s)

Any kitchen subcommand without an instance argument will apply to all instances.

Fixing Converge

Now a colleague has expressed interest in running the cookbook on a fleet of older Ubuntu 10.04 systems. Open .kitchen.yml in your editor and add a ubuntu-10.04 entry to the platforms list:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: ubuntu-10.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:

And run kitchen list to confirm the introduction of our latest instance:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>
default-ubuntu-1004  Vagrant  ChefSolo     <Not Created>
default-centos-64    Vagrant  ChefSolo     <Not Created>

Now we’ll run the test subcommand and go grab a coffee:

$ kitchen test 10
-----> Starting Kitchen (v1.2.1)
-----> Cleaning up any prior instances of <default-ubuntu-1004>
-----> Destroying <default-ubuntu-1004>...
       Finished destroying <default-ubuntu-1004> (0m0.00s).
-----> Testing <default-ubuntu-1004>
...
Chef::Exceptions::Package
       -------------------------
       git has no candidate in the apt-cache

Oh noes! Argh, why!? Let’s login to the instance and see if we can figure out what the correct package is:

$  kitchen login 10

vagrant@default-ubuntu-1004:~$ sudo apt-cache search git | grep ^git
git-buildpackage - Suite to help with Debian packages in Git repositories
git-cola - highly caffeinated git gui
git-load-dirs - Import upstream archives into git
gitg - git repository viewer for gtk+/GNOME
gitmagic - guide about Git version control system
gitosis - git repository hosting application
gitpkg - tools for maintaining Debian packages with git
gitstats - statistics generator for git repositories
git-core - fast, scalable, distributed revision control system
git-doc - fast, scalable, distributed revision control system (documentation)
gitk - fast, scalable, distributed revision control system (revision tree visualizer)
git-arch - fast, scalable, distributed revision control system (arch interoperability)
git-cvs - fast, scalable, distributed revision control system (cvs interoperability)
git-daemon-run - fast, scalable, distributed revision control system (git-daemon service)
git-email - fast, scalable, distributed revision control system (email add-on)
git-gui - fast, scalable, distributed revision control system (GUI)
git-svn - fast, scalable, distributed revision control system (svn interoperability)
gitweb - fast, scalable, distributed revision control system (web interface)

Okay, it looks like we want to install the git-core package for this release of Ubuntu. Let’s fix this up back in the default recipe. Open up recipes/default.rb and edit to something like:

# install needed package
packages = %w(ntp)

if "ubuntu" == node['platform'] && node['platform_version'].to_f <= 10.04
  packages « "git-core"
else
  packages « "git"
end

packages.each do |pack|
  package pack
end

This may not be pretty but let’s verify that it works first on Ubuntu 10.04:

$ kitchen verify 10
...
-----> Running bats test suite
   nginx binary is found in PATH
   nginx config is valid                                                    2/3
   nginx config is valid
   nginx is running                                                         3/3
   nginx is running

       3 tests, 0 failures
       Finished verifying <default-ubuntu-1004> (0m2.51s).
-----> Kitchen is finished. (0m53.48s)

Back to green, good. Let’s verify that the all instances are still good.

$ kitchen verify -c 3
-----> Starting Kitchen (v1.2.1)
-----> Verifying <default-centos-64>...
-----> Verifying <default-ubuntu-1004>...
-----> Verifying <default-ubuntu-1204>...
       Removing /tmp/busser/suites/bats
       Removing /tmp/busser/suites/bats
       Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
       Removing /tmp/busser/suites/bats
       Uploading /tmp/busser/suites/bats/nginx_installed.bats (mode=0755)
-----> Running bats test suite
 -----> Running bats test suite                                             1/3
   nginx binary is found in PATH
   nginx config is valid                                                    2/3
   nginx config is valid
   nginx is running                                                         3/3
   nginx is running

       3 tests, 0 failures
        Finished verifying <default-ubuntu-1004> (0m2.94s).                 1/3
   nginx binary is found in PATH
   nginx config is valid
   nginx is running

       3 tests, 0 failures
       Finished verifying <default-ubuntu-1204> (0m3.09s).
-----> Running bats test suite
   nginx binary is found in PATH
   nginx config is valid                                                    2/3
   nginx config is valid
   nginx is running                                                         3/3
   nginx is running

       3 tests, 0 failures
       Finished verifying <default-centos-64> (0m3.86s).
-----> Kitchen is finished. (0m6.12s)

We used -c to run a test against all matching instances concurrently, where next argument mean number of instances run at the same time.

We’ve successfully verified all three instances, so let’s shut them down.

$ kitchen destroy
-----> Starting Kitchen (v1.2.1)
...
-----> Kitchen is finished. (0m19.86s)

Adding a Suite

We’re going to call our new Test Kitchen Suite «node» by opening .kitchen.yml in your editor of choice so that it looks similar to:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: ubuntu-10.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:
  - name: node
    run_list:
      - recipe[my_cool_app::default]
      - recipe[my_cool_app::node]
    attributes:

Now run kitchen list to see our new suite in action:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>
default-ubuntu-1004  Vagrant  ChefSolo     <Not Created>
default-centos-64    Vagrant  ChefSolo     <Not Created>
node-ubuntu-1204     Vagrant  ChefSolo     <Not Created>
node-ubuntu-1004     Vagrant  ChefSolo     <Not Created>
node-centos-64       Vagrant  ChefSolo     <Not Created>

Writing a Server Test

Now to write a test or two. Previously we’ve seen the bats testing framework in action but this isn’t always a viable option. For example if you needed to verify that a package was installed and you needed to test that on Ubuntu and CentOS platforms, then what would you do? You need to bust out some platform detection in order to run a Debian or RPM-based command. Feels like Chef would help us here since it’s good at that sort of thing. On the other hand there are advantages to treating our Chef run as a black box - a configuration-management implementation detail, if you will. So what to do?

A nice solution to the platform-agnostic test issue exists called ServerSpec. It is a set of RSpec matchers that can assert things about servers like packages installed, services enabled, ports listening, etc. Let’s see what this looks like for our Git Daemon tests.

First we’re going to create a directory for our test file:

$ mkdir -p test/integration/node/serverspec

Next, create a file called test/integration/server/serverspec/nginx_daemon_spec.rb with the following:

require 'serverspec'

include Serverspec::Helper::Exec
include Serverspec::Helper::DetectOS

RSpec.configure do |c|
  c.before :all do
    c.path = '/sbin:/usr/sbin'
  end
end

describe "Nginx Daemon" do

  it "is listening on port 80" do
    expect(port(80)).to be_listening
  end

  it "has a running service of git-daemon" do
    expect(service("nginx")).to be_running
  end

end

And test/integration/server/serverspec/node_spec.rb with the following:

require 'serverspec'

include Serverspec::Helper::Exec
include Serverspec::Helper::DetectOS

RSpec.configure do |c|
  c.before :all do
    c.path = '/sbin:/usr/sbin:/usr/local/bin'
  end
end

describe "Node" do

  describe command('node -v') do
    it { should return_stdout 'v0.10.26' }
  end

end

As our primary target platform was Ubuntu 12.04, we’ll target this one first for development. Now, in Test-Driven style we’ll run kitchen verify to watch our tests fail spectacularly:

$ kitchen verify node-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Creating <node-ubuntu-1204>...
...

Nginx Daemon
  is listening on port 80
  has a running service of git-daemon

       Node
         Command "node -v"

    should return stdout "v0.10.26"

Finished in 0.22341 seconds
3 examples, 0 failures
       Finished verifying <node-ubuntu-1204> (0m2.78s).
-----> Kitchen is finished. (13m29.49s)

One quick check of kitchen list tells us that our instance was created by not successfully converged:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>
default-ubuntu-1004  Vagrant  ChefSolo     <Not Created>
default-centos-64    Vagrant  ChefSolo     <Not Created>
node-ubuntu-1204     Vagrant  ChefSolo     Verified
node-ubuntu-1004     Vagrant  ChefSolo     <Not Created>
node-centos-64       Vagrant  ChefSolo     <Not Created>

Yes, you can specify one or more instances with the same Ruby regular expression globbing as any other kitchen subcommands. Let’s see if our server recipe works on the all platforms (Ubuntu 10.04 and CentOS 6.4). Fingers crossed, here we go:

$ kitchen verify node
-----> Starting Kitchen (v1.2.1)
-----> Verifying <node-ubuntu-1204>...
...

Nginx Daemon
  is listening on port 80
  has a running service of git-daemon

Node
  Command "node -v"
    should return stdout "v0.10.26"

Finished in 0.09353 seconds
3 examples, 0 failures
       Finished verifying <node-ubuntu-1204> (0m2.41s).
-----> Verifying <node-ubuntu-1004>...
...

       Nginx Daemon
         is listening on port 80
         has a running service of git-daemon

       Node
         Command "node -v"
           should return stdout "v0.10.26"

       Finished in 0.08732 seconds
       3 examples, 0 failures
       Finished verifying <node-ubuntu-1004> (0m2.34s).
-----> Verifying <node-centos-64>...
...
       Nginx Daemon
         is listening on port 80
         has a running service of git-daemon

       Node
         Command "node -v"
           should return stdout "v0.10.26"

       Finished in 0.12761 seconds
       3 examples, 0 failures
       Finished verifying <node-centos-64> (0m3.11s).
-----> Kitchen is finished. (0m9.27s)

If for example, you don’t want support some platform, you can use excludes key in .kitchen.yml file:

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: ubuntu-10.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:
  - name: node
    run_list:
      - recipe[my_cool_app::default]
      - recipe[my_cool_app::node]
    attributes:
    excludes:
      - centos-6.4

Now let’s run kitchen list to ensure the instance is gone:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefSolo     <Not Created>
default-ubuntu-1004  Vagrant  ChefSolo     <Not Created>
default-centos-64    Vagrant  ChefSolo     <Not Created>
node-ubuntu-1204     Vagrant  ChefSolo     Verified
node-ubuntu-1004     Vagrant  ChefSolo     Verified

Chef Zero

Chef Zero is a simple, easy-install, in-memory Chef server that can be useful for Chef Client testing and chef-solo-like tasks that require a full Chef Server. It IS intended to be simple, Chef 11 compliant, easy to run and fast to start. It is NOT intended to be secure, scalable, performant or persistent. It does NO input validation, authentication or authorization (it will not throw a 400, 401 or 403). It does not save data, and will start up empty each time you start it.

Because Chef Zero runs in memory, it’s super fast and lightweight. This makes it perfect for testing against a «real» Chef Server without mocking the entire Internet.

Installing

Let’s cover our cookbook by using Chef Zero. First we should add this gem in Gemfile:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'
gem 'thor-foodcritic'
gem 'chefspec'
gem 'test-kitchen'
gem 'kitchen-vagrant'
gem 'chef-zero'

And you should to execute bundle command to install this gem. We can check what Chef Zero installed:

$ chef-zero
» Starting Chef Zero (v1.7.3)...
» Puma (v1.6.3) is listening at http://127.0.0.1:8889
» Press CTRL+C to stop
^C
» Stopping Chef Zero ...

Command chef-zero start Chef Zero server in foreground. This is fully functional (empty) Chef Server. To use it in your own repository, create a knife.rb like so:

chef_server_url   'http://127.0.0.1:8889'
node_name         'stickywicket'
client_key        'path_to_any_pem_file.pem'

And use knife like you normally would.

Since Chef Zero does no authentication, any .pem file will do. The client just needs something to sign requests with (which will be ignored on the server). Even though it’s ignored, the .pem must still be a valid format.

If you will stop the Chef Zero server than all the data is gone.

Run chef-zero --help to see a list of the supported flags and options:

$ chef-zero --help
Usage: chef-zero [ARGS]
    -H, --host HOST                  Host to bind to (default: 127.0.0.1)
    -p, --port PORT                  Port to listen on
        --socket PATH                Unix socket path to listen on
        --[no-]generate-keys         Whether to generate actual keys or fake it (faster).  Default: false.
    -d, --daemon                     Run as a daemon process
    -l, --log-level LEVEL            Set the output log level
    -h, --help                       Show this message
        --version                    Show version

Using with ChefSpec

By default, ChefSpec runs in Chef Solo mode, but you can ask ChefSpec to create an in-memory Chef Server during testing using ChefZero. This is especially helpful if you need to support searching or data bags.

To use the ChefSpec server, simply require the module in your spec_helper:

# spec_helper.rb
require 'chefspec'
require 'chefspec/server'

This will automatically create a Chef server, synchronize all the cookbooks in your cookbook_path, and wire all the internals of Chef together. Recipe calls to search, data_bag and data_bag_item will now query the ChefSpec server.

The ChefSpec server includes a collection of helpful DSL methods for populating data into the Chef Server.

Create a client:

ChefSpec::Server.create_client('my_client', { admin: true })

Create a data bag (and items):

ChefSpec::Server.create_data_bag('my_data_bag', {
  'item_1' => {
    'password' => 'abc123'
  },
  'item_2' => {
    'password' => 'def456'
  }
})

Create an environment:

ChefSpec::Server.create_environment('my_environment', { description: '...' })

Create a node:

ChefSpec::Server.create_node('my_node', { run_list: ['...'] })

You may also be interested in the stub_node macro, which will create a new Chef::Node object and accepts the same parameters as the Chef Runner and a Fauxhai object:

www = stub_node(platform: 'ubuntu', version: '12.04') do |node|
  node.set['attribute'] = 'value'
end

# `www` is now a local Chef::Node object you can use in your test. To push this
# node to the server, call `create_node`:

ChefSpec::Server.create_node(www)

Create a role:

ChefSpec::Server.create_role('my_role', { default_attributes: {} })

# The role now exists on the Chef Server, you can add it to a node's run_list
# by adding it to the `converge` block:
let(:chef_run) { ChefSpec::Runner.new.converge(described_recipe, 'role[my_role]') }

Example

Let’s create recipe haproxy in our cookbook:

# install and setup haproxy

package "haproxy"

# get nodes of some roles
if Chef::Config[:solo]
  Chef::Log.warn("This recipe uses search. Chef Solo does not support search.")
  pool_members = []
else
  pool_members = search("node", "role:#{node['my_cool_app']['haproxy']['app_server_role']} AND chef_environment:#{node.chef_environment}") || []
end

pool_members.map! do |member|
  {:ipaddress => member['ipaddress'], :hostname => member['hostname']}
end

if pool_members.length > 0

  http_clients = pool_members.uniq.map do |s|
    "server #{s[:hostname]} #{s[:ipaddress]}:80 weight 1 maxconn 1024 check"
  end
  http_clients = ["mode http"] + http_clients + ["option httpchk GET /healthcheck"]

  https_clients = pool_members.uniq.map do |s|
    "server #{s[:hostname]} #{s[:ipaddress]}:443 weight 1 maxconn 1024 check"
  end
  https_clients = ["mode http"] + https_clients + ["option httpchk GET /ssl-healthcheck"]

else

  http_clients = https_clients = []

end

listeners = {
  "listen" => {},
  "frontend" => {
    "ft_http" => [
      "bind *:80",
      "mode tcp",
      "default_backend bk_http"
    ],
    "ft_https" => [
      "bind *:443",
      "mode tcp",
      "default_backend bk_https"
    ]
  },
  "backend" => {
    "bk_http" => http_clients,
    "bk_https" => https_clients
  }
}

template "/etc/haproxy/haproxy.cfg" do
  source "haproxy.cfg.erb"
  owner "root"
  group "root"
  mode 00644
  notifies :reload, "service[haproxy]"
  variables(
    :listeners => listeners
  )
end

cookbook_file '/etc/default/haproxy' do
  source 'haproxy-default'
  owner 'root'
  group 'root'
  mode 00644
  notifies :restart, 'service[haproxy]'
end

service "haproxy" do
  supports :restart => true, :status => true, :reload => true
  action [:enable, :start]
end

And default attributes for it:

default['my_cool_app']['haproxy']['app_server_role']       = 'web'

In our recipe used template haproxy.cfg.erb:

global
  log 127.0.0.1   local0
  log 127.0.0.1   local1 notice
  maxconn 5000
  user haproxy
  group haproxy

defaults
  log     global
  mode    http
  retries 3
  timeout client 60000
  timeout server 50000
  timeout connect 25000
  balance roundrobin

<% @listeners.each do |type, listeners | %>
<% listeners.each do |name, listen| %>
<%= type %> <%= name %>
<% listen.each do |option| %>
  <%= option %>
<% end %>
<% end %>
<% end %>

As you can see, this recipe install and configure haproxy. It using search command to get all nodes with role web (by default attributes) and chef environment. After this it collect from all selected nodes hostname and ip address. This data is used to create config for haproxy. This is very useful way, because if you will add or remove node with role web from Chef server, it will automatically update haproxy config. But because we using search command, we need test our cookbook with Chef Zero. Here how it look our tests:

require 'spec_helper'

describe 'my_cool_app::haproxy' do
  let(:platform) { 'ubuntu' }
  let(:platform_version) { '12.04' }
  let(:chef_run) { ChefSpec::Runner.new(platform: platform, version: platform_version).converge(described_recipe) }

  it "install haproxy package" do
    expect(chef_run).to install_package('haproxy')
  end

  it 'enable haproxy service' do
    expect(chef_run).to enable_service('haproxy')
  end

  it 'create config /etc/haproxy/haproxy.cfg with empty backends' do
    expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote("backend bk_http\nbackend bk_https\n")}/)
  end

  context 'with env, role and one node' do
    let(:node_env) { 'test' }
    let(:chef_run) do
      ChefSpec::Runner.new(platform: platform, version: platform_version) do |node|
        # Create a new environment (you could also use a different :let block or :before block)
        env = Chef::Environment.new
        env.name node_env

        # Stub the node to return this environment
        node.stub(:chef_environment).and_return(env.name)

        # Stub any calls to Environment.load to return this environment
        Chef::Environment.stub(:load).and_return(env)
      end.converge(described_recipe)
    end

    before do
      ChefSpec::Server.create_environment(node_env, { description: 'Test env' })
      ChefSpec::Server.create_role('web', { default_attributes: {} })
      ChefSpec::Server.create_node('first-node', {
        run_list: ['role[web]'],
        chef_environment: node_env,
        normal: { fqdn: '127.0.0.1', hostname: 'test.org', ipaddress: '127.0.0.1' }
      })
    end

    it 'create config /etc/haproxy/haproxy.cfg with first-node on 80 port' do
      expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test.org 127.0.0.1:80 weight 1 maxconn 1024 check')}/)
    end

    it 'create config /etc/haproxy/haproxy.cfg with first-node on 443 port' do
      expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test.org 127.0.0.1:443 weight 1 maxconn 1024 check')}/)
    end

    context 'with two nodes' do
      before do
        ChefSpec::Server.create_node('second-node', {
          run_list: ['role[web]'],
          chef_environment: node_env,
          normal: { fqdn: '192.168.1.2', hostname: 'test2.org', ipaddress: '192.168.1.2' }
        })
      end

      it 'create config /etc/haproxy/haproxy.cfg with first-node on 80 port' do
        expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test.org 127.0.0.1:80 weight 1 maxconn 1024 check')}/)
      end

      it 'create config /etc/haproxy/haproxy.cfg with first-node on 443 port' do
        expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test.org 127.0.0.1:443 weight 1 maxconn 1024 check')}/)
      end

      it 'create config /etc/haproxy/haproxy.cfg with second-node on 80 port' do
        expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test2.org 192.168.1.2:80 weight 1 maxconn 1024 check')}/)
      end

      it 'create config /etc/haproxy/haproxy.cfg with second-node on 443 port' do
        expect(chef_run).to render_file('/etc/haproxy/haproxy.cfg').with_content(/#{Regexp.quote('server test2.org 192.168.1.2:443 weight 1 maxconn 1024 check')}/)
      end

    end
  end

end

And we can check our tests:

$ rspec spec/unit/recipes/haproxy_spec.rb
.........

Finished in 33.34 seconds
9 examples, 0 failures

As a result, haproxy recipe fully covered by Chefspec and Chef Zero.

Using with Test Kitchen

To use Chef Zero with test kitchen, you should change provisioner type in .kitchen.yml:

---
driver:
  name: vagrant

provisioner:
  name: chef_zero
  roles_path: "test/chef-zero/roles"
  environments_path: "test/chef-zero/environments"
  nodes_path: "test/chef-zero/nodes"
  client_rb:
    environment: test

platforms:
  - name: ubuntu-12.04
  - name: ubuntu-10.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
    attributes:
  - name: node
    run_list:
      - recipe[my_cool_app::default]
      - recipe[my_cool_app::node]
    attributes:
  - name: haproxy
    run_list:
      - recipe[my_cool_app::haproxy]
    attributes:
    excludes:
      - centos-6.4

As you can see, we changed provisioner name to chef_zero. Also we set roles_path, environments_path and nodes_path. This folders will used to upload to Chef Zero test data - roles, environments and nodes. And we set client_rb attribute, which allow add attributes for chef client. Here we set environment as «test».

Next we create new test suite, called «haproxy». Let’s check it:

$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1204  Vagrant  ChefZero     <Not Created>
default-ubuntu-1004  Vagrant  ChefZero     <Not Created>
default-centos-64    Vagrant  ChefZero     <Not Created>
node-ubuntu-1204     Vagrant  ChefZero     <Not Created>
node-ubuntu-1004     Vagrant  ChefZero     <Not Created>
node-centos-64       Vagrant  ChefZero     <Not Created>
haproxy-ubuntu-1204  Vagrant  ChefZero     <Not Created>
haproxy-ubuntu-1004  Vagrant  ChefZero     <Not Created>

Now lets create folder «integration/haproxy/serverspec» and add to it tests for haproxy recipe:

require 'serverspec'

include Serverspec::Helper::Exec
include Serverspec::Helper::DetectOS

RSpec.configure do |c|
  c.before :all do
    c.path = '/sbin:/usr/sbin'
  end
end

describe "Haproxy Daemon" do

  describe package('haproxy') do
    it { should be_installed }
  end

  describe service('haproxy') do
    it { should be_enabled   }
    it { should be_running   }
  end

  [80, 443].each do |port|
    it "is listening on port #{port}" do
      expect(port(port)).to be_listening
    end
  end

  describe file('/etc/haproxy/haproxy.cfg') do
    it { should be_file }
    its(:content) { should match /#{Regexp.quote('server leopard.in.ua 127.0.0.1:80 weight 1 maxconn 1024 check')}/ }
    its(:content) { should match /#{Regexp.quote('server leopard.in.ua 127.0.0.1:443 weight 1 maxconn 1024 check')}/ }
  end

end

Almost ready. As you can see, we should generate by tests haproxy config with one node (hostname «leopard.in.ua» and ip address «127.0.0.1»). We should prepare this node for tests. In folder «test/chef-zero/nodes» we create node:

{
  "name": "first-node",
  "json_class": "Chef::Node",
  "chef_type": "node",
  "chef_environment": "test",
  "normal": {
    "fqdn": "127.0.0.1",
    "hostname": "leopard.in.ua",
    "ipaddress": "127.0.0.1"
  },
  "run_list": ["role[web]"]
}

It should have «test» environment and use «web» role, because only by this conditions we will select this node for haproxy config. Also we set «hostname» and «ipaddress», because this is not real node and Ohai will not fill this attributes. After this we should create «test» environment and «web» role:

{
  "name": "test",
  "description": "test environment",
  "chef_type": "environment",
  "json_class": "Chef::Environment",
  "default_attributes": {}
}
{
  "name": "web",
  "description": "The web role",
  "chef_type": "role",
  "json_class": "Chef::Role",
  "default_attributes": {
  },
  "run_list": []
}

All this data will load automatically into Chef Zero by Test Kitchen. And now we cat test our haproxy recipe by Test Kitchen:

$ kitchen test haproxy-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Cleaning up any prior instances of <haproxy-ubuntu-1204>
-----> Destroying <haproxy-ubuntu-1204>...
...
Uploading /tmp/busser/suites/serverspec/haproxy_spec.rb (mode=0644)
-----> Running serverspec test suite
/opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec -S /opt/chef/embedded/bin/rspec /tmp/busser/suites/serverspec/haproxy_spec.rb --color --format documentation

       Haproxy Daemon
  is listening on port 80
  is listening on port 443
  Package "haproxy"
    should be installed
  Service "haproxy"
    should be enabled
    should be running
  File "/etc/haproxy/haproxy.cfg"
    should be file
    content
      should match /server\ leopard\.in\.ua\ 127\.0\.0\.1:80\ weight\ 1\ maxconn\ 1024\ check/
    content
      should match /server\ leopard\.in\.ua\ 127\.0\.0\.1:443\ weight\ 1\ maxconn\ 1024\ check/

Finished in 0.55591 seconds
8 examples, 0 failures
       Finished verifying <haproxy-ubuntu-1204> (0m2.20s).
...
       Finished testing <haproxy-ubuntu-1204> (3m26.69s).
-----> Kitchen is finished. (3m28.05s)

As a result, haproxy recipe fully covered by Test Kitchen, Serverspec and Chef Zero.

Minitest

Minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking. Minitest doesn’t reinvent anything that ruby already provides, like: classes, modules, inheritance, methods. This means you only have to learn ruby to use minitest and all of your regular OO practices like extract-method refactorings still apply.

Exists two way of usage minitest:

Second way is very interesting, because allow to you check tests even on production environment. This allow to be sure, what tests pass on check run of chef client on node.

Let’s consider each example.

Test Kitchen

As you remember, we used bats and serverspec with Test Kitchen. But inside it you can use any test framework, for which exists «busser». Right now you can find:

Let’s add little test inside our cookbook:

require 'minitest/autorun'

describe "my_cool_app::default" do

  it "has created /var/www/my_cool_app/index.html" do
    assert File.exists?("/var/www/my_cool_app/index.html")
  end

end

And check how it works:

$ kitchen test default-ubuntu-1204
-----> Starting Kitchen (v1.2.1)
-----> Cleaning up any prior instances of <default-ubuntu-1204>
-----> Destroying <default-ubuntu-1204>...
...
       Plugin minitest installed (version 0.2.0)
-----> Running postinstall for minitest plugin
...
-----> Running minitest test suite
/opt/chef/embedded/bin/ruby  -I"/opt/chef/embedded/lib/ruby/1.9.1" "/opt/chef/embedded/lib/ruby/1.9.1/rake/rake_test_loader.rb" "/tmp/busser/suites/minitest/test_default.rb"
Run options: --seed 59554

# Running tests:

.

Finished tests in 0.002407s, 415.5347 tests/s, 415.5347 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
       Finished verifying <default-ubuntu-1204> (0m2.39s).
-----> Kitchen is finished. (1m27.97s)

As you can see, minitest doesn’t have additional helpers, like have serverspec (be_listening, be_running, etc). What is why let’s consider second way of using it.

Minitest Chef Handler

Minitest-chef-handler run minitest suites after your Chef recipes to check the status of your system. It can be very useful, because you testing inside your real environment. Exists 2 option to use it:

Option 1: Add the report handler to your client.rb or solo.rb file:

require 'minitest-chef-handler'

report_handlers « MiniTest::Chef::Handler.new

Option 2: Using minitest-handler cookbook, which should be added in the end of run_list:

chef.run_list = [
  "your-recipes",
  "minitest-handler"
]

I prefer second variant. Just add in Berksfile cookbook minitest-handler:

source "http://api.berkshelf.com"

metadata

cookbook "minitest-handler"

Add it in run_list:

suites:
  - name: default
    run_list:
      - recipe[my_cool_app::default]
      - recipe[minitest-handler]

Next we should write some tests. Let’s create folder to its:

$ mkdir -p files/default/test

And add some tests to cover default recipe:

require 'minitest/spec'

describe_recipe 'my_cool_app::default' do

  it "install ntp package" do
    package('ntp').must_be_installed
  end

  it "install git package" do
    if "ubuntu" == node['platform'] && node['platform_version'].to_f <= 10.04
      pack = "git-core"
    else
      pack = "git"
    end
    package(pack).must_be_installed
  end

  it "nginx must running" do
    service("nginx").must_be_running
  end

end

Finally run our chef-client by Test Kitchen «converge» command:

$ kitchen converge default-ubuntu-1204
...
Running handlers:
[2014-03-27T20:30:03+00:00] INFO: Running report handlers
Run options: -v --seed 42106

# Running tests:

recipe::my_cool_app::default#test_0002_install git package = 0.14 s = .
recipe::my_cool_app::default#test_0001_install ntp package = 0.08 s = .
recipe::my_cool_app::default#test_0003_nginx must running = 0.09 s = .

As a result, after launching all recipes in run_list, last recipe minitest-handler::default run minitest to check status of the node.

Cucumber

Cucumber is a tool for running automated tests written in plain language. Because they’re written in plain language, they can be read by anyone on your team. Because they can be read by anyone, you can use them to help improve communication, collaboration and trust on your team.

Example

Therefore we need only add the following three gems to a Gemfile:

gem 'cucumber'
gem 'rspec-expectations'
gem 'leibniz'

And you should to execute bundle command to install this gem.

Next we should create directory for our tests:

$ mkdir -p features/step_definitions
$ mkdir -p features/step_definitions/support

And require needed libs in features/step_definitions/support/env.rb file:

require 'leibniz'
require 'faraday'

Now we should create file, which will contain tests definitions. Let’s call it features/working_web_page.feature:

Feature: Customer can use my cool web app

  In order to get more payment customers
  As a business owner
  I want web users to be able use my cool web app

  Background:
    Given I have provisioned the following infrastructure:
    | Server Name | Operating System | Version | Chef Version | Run List              |
    | localhost   | ubuntu           | 12.04   | 11.4.4       | my_cool_app::default  |

    And I have run Chef

  Scenario: User visits home page
    Given a url "http://example.org"
    When a web user browses to the URL
    Then the user should see "This is my cool web app"
    And cleanup test env

Next, we can try run cucumber:

$ cucumber
Feature: Customer can use my cool web app

  In order to get more payment customers
  As a business owner
  I want web users to be able use my cool web app

  Background:                                              # features/working_web_page.feature:7
    Given I have provisioned the following infrastructure: # features/working_web_page.feature:8
      | Server Name | Operating System | Version | Chef Version | Run List             |
      | localhost   | ubuntu           | 12.04   | 11.4.4       | my_cool_app::default |
    And I have run Chef                                    # features/working_web_page.feature:12

  Scenario: User visits home page                          # features/working_web_page.feature:14
    Given a url http://example.org                         # features/working_web_page.feature:15
    When a web user browses to the URL                     # features/working_web_page.feature:16
    Then the user should see "This is my cool web app"     # features/working_web_page.feature:17

1 scenario (1 undefined)
5 steps (5 undefined)
0m0.012s

You can implement step definitions for undefined steps with these snippets:

Given(/^I have provisioned the following infrastructure:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

Given(/^I have run Chef$/) do
  pending # express the regexp above with the code you wish you had
end

Given(/^a url http:\/\/example\.org$/) do
  pending # express the regexp above with the code you wish you had
end

When(/^a web user browses to the URL$/) do
  pending # express the regexp above with the code you wish you had
end

Then(/^the user should see "(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

If you want snippets in a different programming language,
just make sure a file with the appropriate file extension
exists where cucumber looks for step definitions.

As you can see, we dont have any line of real tests. Let’s add tests in file features/step_definitions/working_web_page.rb:

Given(/^I have provisioned the following infrastructure:$/) do |specification|
  @infrastructure = Leibniz.build(specification)
end

Given(/^I have run Chef$/) do
  @infrastructure.destroy
  @infrastructure.converge
end

Given(/^a url "(.*?)"$/) do |url|
  @host_header = url.split('/').last
end

When(/^a web user browses to the URL$/) do
  connection = Faraday.new(:url => "http://#{@infrastructure['localhost'].ip}", :headers => {'Host' => @host_header}) do |faraday|
    faraday.adapter Faraday.default_adapter
  end
  @page = connection.get('/').body
end

Then(/^the user should see "(.*?)"$/) do |content|
  expect(@page).to match /#{content}/
end

Then(/^cleanup test env$/) do
  @infrastructure.destroy if @infrastructure
end

Leibniz gem read infrastructure configuration from our specs inside «Background» and use Test Kitchen to create it. Next, in «I have run Chef» we cleanup old and create new node, install chef client and run it inside node. After this we using Faraday gem to do HTTP request inside node and get root page content. We are checking, what this content should contain «This is my cool web app». In this case we will be sure, what nginx installed, running and serve our web page. In the end we added «cleanup test env», which would remove node after tests.

Finally, we cat test our cucumber tests:

$ cucumber
Feature: Customer can use my cool web app

  In order to get more payment customers
  As a business owner
  I want web users to be able use my cool web app

  Background:                                              # features/working_web_page.feature:7
    Given I have provisioned the following infrastructure: # features/step_definitions/working_web_page.rb:1
      | Server Name | Operating System | Version | Chef Version | Run List             |
      | localhost   | ubuntu           | 12.04   | 11.4.4       | my_cool_app::default |
-----> Destroying <leibniz-localhost>...
       ==> default: Forcing shutdown of VM...
       ==> default: Destroying VM and associated drives...
...

Chef Client finished, 28 resources updated
       Finished converging <leibniz-localhost> (1m6.98s).
    And I have run Chef                                    # features/step_definitions/working_web_page.rb:5

  Scenario: User visits home page                          # features/working_web_page.feature:14
    Given a url "http://example.org"                       # features/step_definitions/working_web_page.rb:10
    When a web user browses to the URL                     # features/step_definitions/working_web_page.rb:14
    Then the user should see "This is my cool web app"     # features/step_definitions/working_web_page.rb:21
-----> Destroying <leibniz-localhost>...
       ==> default: Forcing shutdown of VM...
       ==> default: Destroying VM and associated drives...
       Vagrant instance <leibniz-localhost> destroyed.
       Finished destroying <leibniz-localhost> (0m7.15s).
    And cleanup test env                                   # features/step_definitions/working_web_page.rb:25

1 scenario (1 passed)
6 steps (6 passed)
2m41.004s

As a result, we cover our default recipe by using cucumber.

Static Analysis and Linting Tools

Foodcritic

Foodcritic is a lint tool for your Opscode Chef cookbooks. Foodcritic has two goals:

On main site you can find list of rules. Also you can define own list of rules (if you need this). Foodcritic is like jslint for cookbooks. At the bare minimum, you should run foodcritic against all your cookbooks.

Example

We need to add foodcritic gems in Gemfile inside our my_cool_app cookbook:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'

And you should to execute bundle command to install this gem. Next we can to check my_cool_app cookbook by foodcritic:

$ foodcritic .
FC015: Consider converting definition to a LWRP: ./definitions/enable_web_site.rb:1
FC021: Resource condition in provider may not behave as expected: ./providers/know_host.rb:39
FC048: Prefer Mixlib::ShellOut: ./libraries/provider_known_host.rb:56
FC048: Prefer Mixlib::ShellOut: ./providers/know_host.rb:33

We have a few warnings in the code. Let’s fix them:

my_cool_app/libraries/provider_known_host.rb
@@ -53,7 +53,8 @@ class Chef
       private

       def insure_for_file(new_resource)
-        key = (new_resource.key || `ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1`)
+        cmd = Mixlib::ShellOut.new("ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1")
+        key = (new_resource.key || cmd.run_command.stdout)
         comment = key.split("\n").first || ""

my_cool_app/providers/know_host.rb
@@ -30,13 +30,15 @@ action :delete do
 end

 def insure_for_file(new_resource)
-  key = (new_resource.key || `ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1`)
+  cmd = Mixlib::ShellOut.new("ssh-keyscan -H -p #{new_resource.port} #{new_resource.host} 2>&1")
+  key = (new_resource.key || cmd.run_command.stdout)
   comment = key.split("\n").first || ""

   Chef::Application.fatal! "Could not resolve #{new_resource.host}" if key =~ /getaddrinfo/

   # Ensure that the file exists and has minimal content (required by Chef::Util::FileEdit)
-  file new_resource.known_hosts_file do
+  file "Check what file #{new_resource.known_hosts_file} exists for #{new_resource.name}" do
+    path          new_resource.known_hosts_file
     action        :create
     backup        false
     content       '# This file must contain at least one line. This is that line.'

And run again foodcritic:

$  foodcritic .
FC015: Consider converting definition to a LWRP: ./definitions/enable_web_site.rb:1

As you can see almost all warnings fixed.

Integration by Rake

Foodcritic has unreleased experimental support for Rake included. With foodcritic and rake in your Gemfile your Rakefile would look like this:

require 'foodcritic'
task :default => [:foodcritic]
FoodCritic::Rake::LintTask.new

You can also pass a block when instantiating to configure the lint options:

require 'foodcritic'
task :default => [:foodcritic]
FoodCritic::Rake::LintTask.new do |t|
  t.options = {:fail_tags => ['correctness']}
end

Integration by Thor

While Rake is the old grand-daddy of Ruby build tools, a number of people prefer to use Thor.

We need to add thor-foodcritic gem in Gemfile inside our my_cool_app cookbook:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'
gem 'thor-foodcritic'

And you should to execute bundle command to install this gem. Add the FoodCritic tasks to Thorfile:

# encoding: utf-8

require 'bundler'
require 'bundler/setup'
require 'berkshelf/thor'
require 'thor/foodcritic'

And then get a list of your thor tasks:

$ thor list
...
foodcritic
----------
thor foodcritic:lint  # Run a lint test against the specified Cookbook and Role paths or otherwise your current working directory.

...

Run the lint task to get a review:

$ thor foodcritic:lint
FC015: Consider converting definition to a LWRP: /Users/leo/Documents/chef_book/code/my-server-cloud/site-cookbooks/my_cool_app/definitions/enable_web_site.rb:1

Get info about options:

$ thor help foodcritic:lint
Usage:
  thor foodcritic:lint

Options:
  -B, [--cookbook-path=one two three]  # Cookbook path(s) to check.
                                       # Default: /Users/leo/Documents/chef_book/code/my-server-cloud/site-cookbooks/my_cool_app
  -R, [--role-path=one two three]      # Role path(s) to check.
  -t, [--tags=one two three]           # Only check against rules with the specified tags.
  -I, [--include=one two three]        # Additional rule file path(s) to load.
  -f, [--epic-fail=one two three]      # Fail the build if any of the specified tags are matched.
  -e, [--exclude-paths=one two three]  # Paths to exclude when running tests.
                                       # Default: ["test/**/*", "spec/**/*", "features/**/*"]

Run a lint test against the specified Cookbook and Role paths or otherwise your current working directory.

We can ignore some rules by using tags. For example, to ignore FC015 warning, we can run command:

$ thor foodcritic:lint -t ~FC015

$

Or we can use .foodcritic file inside our cookbook folder and add tags inside it:

$ cat .foodcritic
~FC015

$ thor foodcritic:lint

$

Here we use the tilde ~ to exclude FC015. For example, if we need check only rules, which have tags services and style, we use directly by foodcritic command:

$ foodcritic -t style,services .

$

Or with Thor:

$ thor foodcritic:lint -t style,services

$

As you can see, in this case you can add this command in your Continuous integration (CI) and check your cookbook by foodcritic.

Extra Rules

Except standart rules exists additional rules that you can use with foodcritic:

And, of course, you can write own number of rules for your cookbooks.

RuboCop

RuboCop is a Ruby static code analyzer. Out of the box it will enforce many of the guidelines outlined in the community Ruby Style Guide.

Example

First we should add this gem in Gemfile:

source 'https://rubygems.org'

gem 'rubocop', require: false

And you should to execute bundle command to install this gems.

After this you can use command line tool «rubocop» to check your Ruby code by community style guide:

$ rubocop
Inspecting 24 files
CCCCWCCCCCCCCCCCCCCCWWCC

Offenses:

attributes/default.rb:4:55: C: Source files should end with a newline (\n).
default['my_cool_app']['web_host']      = 'example.org'
                                                      ^

...

test/integration/node/serverspec/nginx_daemon_spec.rb:18:6: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  it "has a running service of nginx" do
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test/integration/node/serverspec/nginx_daemon_spec.rb:19:20: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
    expect(service("nginx")).to be_running
                   ^^^^^^^
test/integration/node/serverspec/nginx_daemon_spec.rb:22:3: C: Source files should end with a newline (\n).
end
  ^
test/integration/node/serverspec/node_spec.rb:12:10: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
describe "Node" do
         ^^^^^^
test/integration/node/serverspec/node_spec.rb:18:3: C: Source files should end with a newline (\n).
end
  ^
24 files inspected, 229 offenses detected

As you can see, this code contain many problems in Ruby style, which can be fixed.

The behavior of RuboCop can be controlled via the .rubocop.yml configuration file. It makes it possible to enable/disable certain cops (checks) and to alter their behavior if they accept any parameters. The file can be placed either in your home directory or in some project directory.

Strainer

Strainer is a gem for isolating and testing individual chef cookbooks. It allows you to keep all your cookbooks in a single repository (saving you time and money), while having all the benefits of individual repositories. But also you can use Strainer in «standalone» mode. This allows you to use Strainer file from within a cookbook.

Example

First, add it in Gemfile:

source 'https://rubygems.org'

gem 'berkshelf'
gem 'foodcritic'
gem 'thor-foodcritic'
gem 'chefspec'
gem 'test-kitchen'
gem 'chef-zero'

group :integration do
  gem 'kitchen-vagrant'
  gem 'cucumber'
  gem 'rspec-expectations'
  gem 'leibniz', '~> 0.2.1'
end

gem 'rubocop', require: false
gem 'strainer', require: false

And run «bundle» command to install it.

Next you must create file Strainerfile with such content:

chefspec:   bundle exec rspec --color
kitchen:    bundle exec thor kitchen:default-ubuntu-1204

Strainer have similar functionality as Foreman, but for running tests inside cookbook(s). Now we cat test our cookbook:

$ bundle exec strainer test
# Straining 'my_cool_app (v0.1.0)'
chefspec             | bundle exec rspec --color
chefspec             | ..................
chefspec             | Finished in 1 minute 22.43 seconds
chefspec             | 23 examples, 0 failures
chefspec             | SUCCESS!
kitchen              | bundle exec thor kitchen:default-ubuntu-1204
kitchen              | -----> Cleaning up any prior instances of <default-ubuntu-1204>
kitchen              | -----> Destroying <default-ubuntu-1204>...

...

kitchen              |        Vagrant instance <default-ubuntu-1204> destroyed.
kitchen              |        Finished destroying <default-ubuntu-1204> (0m6.87s).
kitchen              |
kitchen              | SUCCESS!
Strainer marked build OK

Also you can use it with Rake or Thor command line tools.

Summary

Chef cookbook testing is very important part of cookbook development. Testing process allow for developers and devops to be sure, what important parts of cookbook working as expected and new updated and features will not break cookbook workflow.

Tips and Tricks

Wrapper cookbook

A wrapper cookbook wraps an upstream cookbook to change its behavior without forking it.

There are two main reasons you might want to do this:

Codifying Standards in Your Organization

Suppose I use the community ntp cookbook but I want to enforce a set of timeservers across my infrastructure. Instead of running this cookbook directly, I could create an acmeco-ntp cookbook with the following settings:

default['ntp']['peers'] = ['ntp1.acmeco.com', 'ntp2.acmeco.com']
include_recipe 'ntp'

Now I can simply run recipe[acmeco-ntp] in my infrastructure and the default settings will take effect.

Note that it is not necessary to use normal or override priority here. Dependent cookbooks are loaded first by Chef Client and their attribute files are evaluated before those of the caller.

Modifying Upstream Cookbook Behavior

Sometimes you want to modify the behavior of an upstream cookbook without forking it. For example, let’s take the PostgreSQL community cookbook. It installs whatever PostgreSQL packages come from your operating system distribution. Suppose you want to install version 9.4 of PostgreSQL on an operating system that would not natively provide it (e.g. RedHat Enterprise Linux 6) but those packages can be found in the official PostgreSQL Global Development Group (PGDG) repository. How would you go about doing that? You could write a wrapper cookbook that set the right attributes:

default['postgresql']['version'] = '9.4'
default['postgresql']['client']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-devel"]
default['postgresql']['server']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-server"]
default['postgresql']['contrib']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-contrib"]
default['postgresql']['dir'] = "/var/lib/pgsql/#{node['postgresql']['version']}/data"
default['postgresql']['server']['service_name'] = "postgresql-#{node['postgresql']['version']}"
include_recipe 'postgresql::yum_pgdg_postgresql'
include_recipe 'postgresql::server'

What’s with the repetition of computed attributes in the wrapper? Well, the values for default['postgresql']['client']['packages'] and so on were calculated when the attributes were loaded by the dependency, so to recompute them based on the new value, we need to restate the expressions.

You could do all of this work in roles as well — and if you do, the computed attributes will be correctly resolved without this kind of repetition. This is another reason that roles are still valuable.

You can take this one step further: suppose you wanted to then derive the pg_hba.conf (the database access control file in PostgreSQL) through some external mechanism that isn’t supported in the upstream cookbook. No problem: you can also set an attribute in recipe context, before the include_recipe statements above:

pg_hba_hash = call_some_method_to_get_a_hash()
node.default['postgresql']['pg_hba'] = pg_hba_hash

Again, in recipe context, there is no need to use normal or override priority to achieve the desired effect.

Advanced Upstream Cookbook Modification

You can also use wrapper cookbooks to manipulate Chef’s Resource Collection. Put simply, the resource collection is the ordered list of resources, from the recipes in your expanded run list, that are to be run on a node. You can manipulate attributes of the resources in the resource collection. One common use case for this is to change the template used by an upstream cookbook to the caller’s cookbook. Again, suppose I’m using the PostgreSQL cookbook but I really hate the sysconfig template that it uses. I can simply make my own template inside the wrapper cookbook:

PGDATA=<%= node['postgresql']['dir'] %>
<% if node['postgresql']['config'].attribute?("port") -%>
PGPORT=<%= node['postgresql']['config']['port'] %>
<% end -%>
PGCHEFS="Ohai" # or whatever changes you want to make

and «rewind» the resource collection definition after that resource has been loaded by recipe[postgresql::server] to change its cookbook attribute:

include_recipe 'postgresql::yum_pgdg_postgresql'
include_recipe "postgresql::server"

resources("template[/etc/sysconfig/pgsql/#{node['postgresql']['server']['service_name']}]").cookbook 'acmeco-postgresql'

You can play this game with any other parameters to a previously defined resource that you want to change. Because Chef uses a two-phase execution model (compile, then converge), you can manipulate the results of that compilation in many different ways before convergence happens.

Chef Rewind gem will also do this kind of manipulation.

Summary

Wrapper cookbooks allow you to modify the behavior of upstream cookbooks without forking them. These modifications can be very straightforward, such as you might do with a role, except that they can contain logic to govern the changes you want to make. Or the modifications can get quite advanced, through altering the resources in the resource collection.

It’s useful to name your wrapper cookbooks with a standard prefix that denotes your organization (e.g. «oc-» is what we use at Opscode). That distinguishes your wrapper from the cookbook you’re wrapping.

Finally, you need not strictly adopt only wrapper cookbooks or only roles. Used effectively, both roles and wrapper cookbooks give you a wealth of tools to model your infrastructure effectively.

Knife Plugins

A Knife plugin is a set of one (or more) subcommands that can be added to Knife to support additional functionality that is not built-in to the base set of Knife subcommands. Many of the Knife plugins are built by members of the Chef community and several of them are built and maintained by Chef. A Knife plugin is installed to the ~/.chef/plugins/knife/ directory, from where it can be run just like any other Knife subcommand.

The following Knife plug-ins are maintained by Chef:

Examples

Let’s consider an example of using knife ec2 plugin. First of all you should install it. You can use bundler or rubugems:

$ gem install knife-ec2

The server create argument is used to create a new Amazon EC2 cloud instance. This will provision a new image in Amazon EC2, perform a bootstrap (using the SSH protocol), and then install the chef-client on the target system so that it can be used to configure the node and to communicate with a Chef server.

To launch a new Amazon EC2 instance with the «webserver» role (Ubuntu Server 14.04 LTS (HVM), c3.large type):

$ knife ec2 server create -r "role[webserver]" -I ami-a6926dce -f c3.large -G www,default -x ubuntu -N server01 -A aws-access-key-id -K aws-secret-access-key -S aws/servers.pem

Let’s look at the meaning of these options:

Some of this keys possible to setup inside knife.rb:

$ cat ~/.chef/knife.rb
knife[:availability_zone] = ENV['EC2_AVAILABILITY_ZONE']
knife[:aws_access_key_id] = ENV['AWS_ACCESS_KEY_ID']
knife[:aws_secret_access_key] = ENV['AWS_SECRET_ACCESS_KEY']
knife[:aws_ssh_key_id] = "aws/servers.pem"
knife[:image] = "ami-a6926dce"
knife[:flavor] = "c3.large"
knife[:chef_mode] = "solo"
knife[:region] = ENV['EC2_REGION']

And now server create command has a different look:

$ AWS_ACCESS_KEY_ID=aws-access-key-id AWS_SECRET_ACCESS_KEY=aws-secret-access-key knife ec2 server create -r "role[webserver]" -G www,default -x ubuntu -N server01

The server list argument is used to find instances that are associated with a Amazon EC2 account. The results may show instances that are not currently managed by the Chef server.

$ AWS_ACCESS_KEY_ID=aws-access-key-id AWS_SECRET_ACCESS_KEY=aws-secret-access-key knife ec2 server list
Instance ID  Name                     Public IP       Private IP      Flavor     Image         SSH Key       Security Groups  IAM Profile  State
i-xxx   staging-fix                   25.196.195.41   11.31.53.118    m1.medium  ami-f62cdf9e  some_key      db,web                        running
i-xxx   Staging::Web                                                  m1.medium  ami-3d4ff254  some_key      web                           stopped

The server delete argument is used to delete one or more nodes that are running in the Amazon EC2 cloud. To find a specific cloud instance, use the knife ec2 server list argument. Use the --purge option to delete all associated node and client objects from the Chef server or use the knife node delete and knife client delete sub-commands to delete specific node and client objects.

$ AWS_ACCESS_KEY_ID=aws-access-key-id AWS_SECRET_ACCESS_KEY=aws-secret-access-key knife ec2 server delete i-xxx

As you can see in these examples knife plugins simplify working with hosting cloud providers.

Chef Metal

Chef Metal solves the problem of repeatably creating machines and infrastructures in Chef. It has a plugin model that lets you write bootstrappers for your favorite infrastructures, including VirtualBox, EC2, LXC, bare metal, and many more. Combined with the power of Chef, Metal’s machine resource helps you to describe, version, deploy and manage everything from simple to complex clusters with a common set of tools.

Examples

First of all, we should create file structure of project:

$ tree -R -a .
.
|--.chef
|.. -- knife.rb
|--.ruby-version
|-- Berksfile
|-- Berksfile.lock
|-- cluster.rb
|-- Gemfile
|-- Gemfile.lock
|-- destroy_all.rb
 -- vagrant.rb

File knife.rb contain setting for knife client:

# This file exists mainly to ensure we don't pick up knife.rb from anywhere else
local_mode true
config_dir "#{File.expand_path('..', __FILE__)}/" # Wherefore art config_dir, chef?

# Chef 11.14 binds to "localhost", which interferes with port forwarding on IPv6 machines for some reason
begin
  chef_zero.host '127.0.0.1'
rescue
end

Chef Metal uses Chef Zero to create cluster of nodes.

Next we should install chef-zero rubygem. Also we should select which provisioner we will use. For our example we will use Vagrant, what is why we need install also chef-metal-vagrant rubygem. In this example uses bundler to install all rubygems. This is content of Gemfile file:

source "http://rubygems.org"

gem 'chef-metal'
gem 'chef-metal-vagrant'

And run bundle command in terminal to install all rubygems.

In this example uses berkshelf to get all needed cookbooks. Content of Berksfile file:

source "http://api.berkshelf.com"

cookbook 'apache2'
cookbook 'mysql'

And run berks install && berks vendor cookbooks command in terminal to install and put all cookbooks to cookbooks directory.

All configuration will contains in files vagrant.rb (file contain configuration for provisioner):

require 'chef_metal_vagrant'

vagrant_box 'precise64' do
  url 'http://files.vagrantup.com/precise64.box'
end

with_machine_options :vagrant_options => {
  'vm.box' => 'precise64'
}

and cluster.rb (file contain configuration for our cluster):

require 'chef_metal'

WEB_NODES = 2

1.upto(WEB_NODES) do |i|
  machine "web#{i}" do
    tag 'web'
    recipe 'apache2'
    converge true
  end
end

machine 'db' do
  tag 'db'
  recipe 'mysql::server'
  converge true
end

By this setting we will create two web nodes and one database node. Now we are ready to create our cluster of nodes:

$ chef-client -z vagrant.rb cluster.rb
Starting Chef Client, version 11.14.2
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Compiling Cookbooks...
[2014-08-02T21:22:49+03:00] WARN: Node alexeys-mbp-2 has an empty run list.
Converging 2 resources
Recipe: @recipe_files::/Users/leo/Downloads/chef-metal/vagrant.rb
  * vagrant_box[precise64] action create (up to date)
Recipe: @recipe_files::/Users/leo/Downloads/chef-metal/cluster.rb
  * machine_batch[default] action converge

...

Running handlers complete
Chef Client finished, 0/18 resources updated in 46.728927972 seconds

If you need more web nodes you just need increase WEB_NODES variable and run Chef Metal again.

To cleanup all nodes I created destroy_all.rb file:

require 'chef_metal'

machine_batch do
  machines search(:node, '*:*').map { |n| n.name }
  action :destroy
end

As a result you should see this output:

$ chef-client -z destroy_all.rb
Starting Chef Client, version 11.14.2
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Compiling Cookbooks...
[2014-08-02T21:46:26+03:00] WARN: Node alexeys-mbp-2 has an empty run list.
Converging 1 resources
Recipe: @recipe_files::/Users/leo/Downloads/chef-metal/destroy_all.rb
  * machine_batch[default] action destroy
    - run vagrant destroy -f db web1 web2 (status was 'running, running, running')
    - delete node db at http://127.0.0.1:8889
    - delete client db at http://127.0.0.1:8889
    - delete file /Users/leo/Downloads/chef-metal/.chef/vms/db.vm
    - delete node web1 at http://127.0.0.1:8889
    - delete client web1 at http://127.0.0.1:8889
    - delete file /Users/leo/Downloads/chef-metal/.chef/vms/web1.vm
    - delete node web2 at http://127.0.0.1:8889
    - delete client web2 at http://127.0.0.1:8889
    - delete file /Users/leo/Downloads/chef-metal/.chef/vms/web2.vm

Running handlers:
Running handlers complete
Chef Client finished, 1/1 resources updated in 32.419881 seconds

As a result, we have DSL to describe our infrastructure (this is more «visible» for DevOps engineers) and can be used to describe, version, deploy and manage everything from simple to complex clusters with a common set of tools. Also Chef Metal is working on top of Chef, so it is very simple to extend and modify your cluster configuration.

Chef Sugar

Chef Sugar is a rubygem and Chef recipe that includes series of helpful sugar of the Chef core and other resources to make a cleaner, more lean recipe DSL, enforce DRY principles, and make writing Chef recipes an awesome experience. This is very useful library, which have huge amount of helpers and help for developers to not «reinvent the wheel» inside own cookbooks.

Usage

First of all, you should add Chef Sugar inside own cookbook metadata.rb file as dependency:

depends 'chef-sugar'

In order to use Chef Sugar in your Chef Recipes, you’ll first need to include it:

include_recipe 'chef-sugar'

Alternatively you can put it in a base role or recipe and it will be included subsequently.

Requiring the Chef Sugar Gem will automatically extend the Recipe DSL, Chef::Resource and Chef::Provider with helpful convenience methods. If you are working outside of the Recipe DSL, you can use the module methods instead of the Recipe DSL. In general, the module methods have the same name as their Recipe DSL counterparts, but require the node object as a parameter. For example:

In recipe:

# cookbook/recipes/default.rb
do_something if windows?

In a library as a singleton:

# cookbook/libraries/default.rb
def only_on_windows(&block)
  yield if Chef::Sugar::PlatformFamily.windows?(@node)
end

In a library as a mixin:

# cookbook/libraries/default.rb
include Chef::Sugar::PlatformFamily

def only_on_windows(&block)
  yield if windows?(@node)
end

Chef Sugar have huge amount of helper methods, more information about its you can found in README. Examples:

execute 'build[my binary]' do
  command '...'
  not_if  { _64_bit? } # check system 64 bit
end
if ubuntu? # system is ubuntu
  execute 'apt-get update'
end
if includes_recipe?('apache2::default') # determines if the current run context includes the recipe
  apache_module 'my_module' do
    # ...
  end
end

Summary

Chef is very flexible DevOps tool. For it exists huge amount good vendor cookbooks, libraries and extensions, which allow developers and administrators use it as they see own infrastructure.

9

Alexey Vasiliev aka leopard: Chef articles http://leopard.in.ua/categories.html#chef-ref

All about Chef ... http://docs.opscode.com/

Doing Wrapper Cookbooks Right http://www.getchef.com/blog/2013/12/03/doing-wrapper-cookbooks-right/