Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Forked and adjusted notes for creating a sample cookbook and running it through test kitchen with docker.

ChefDK, Test Kitchen Driven NTP Cookbook via Docker

This gist is a fork from lamont-granquist modified to be even easier and faster by taking advantage of Docker. Given the rapid development and change rate of Chef finding this gist with instructions that still are valid and reasonable was much appreciated.

This gist uses TK+Berkshelf to drive creating a vagrant virts and converging a simple recipe to install and configure NTPd. This is a simple cookbook that has one recipe, one template (for ntp.conf) and one attribute file. It works on Ubuntu 12.04/14.04 and CentOS 6.4/7.1 (and derviatives) and the attribute file is used to support both distros.

This should work on Mac (where I developed it) and any chef-supported Linux that you can get Docker onto (Ubuntu/CentOS).

Because I use ChefDK and Test Kitchen, I can largely ignore setting up Docker and Berkshelf and can get right to work on writing recipe code.

NOTE: Modern (7/6/2014) Recipe Generation

  • We do not create a Vagrantfile (TK does that for us)
  • We do not use vagrant-omnibus or vagrant-berkshelf plugins (TK does that for us)
  • We use ChefDK (aka the chef command) to generate Berkshelf and .kitchen.yml files so we don't have to touch those
  • We use the ChefDK package to install everything for us (except vagrant)
  • We don't use "knife cookbook create" and instead use "chef generate cookbook"

NOTE: Even more modern (9/19/2015) Recipe Generation

  • Updated to use current versions of Ubuntu and Centos (no meaningful changes to workflow for them)
  • We use kitchen-docker for bringing up test instances so no need to create or manage a Dockerfile
  • We use the newer chef-zero rather than chef-solo

If you're trying to mix+match these instructions with other HOWTOs on the Internet that have you editing your own Vagrantfile or installing vagrant plugins like vagrant-omnibus or vagrant-berkshelf the you'll probably have a bad time. What this HOWTO is tring to do is leverage TK and Berkshelf (and ChefDK to configure both of those for you) in order so that you can quickly move on to converging chef recipes on Ubuntu and CentOS virtual machines.

This is the correct way to start building Chef Cookbooks and leverage ChefDK and Test Kitchen as of this writing. This HOWTO should not build any bad habits, and the cookbook that results from this will only need to be extended to include tests.

Not that we're not using Test Kitchen to actually test our cookbook. We are 'only' using it to run docker and berkshelf, sync our cookbook(s), install and/or update the chef-client on the virtual node that we provision, and converge the node (which is quite a lot really -- test-kitchen has to do a lot of heavy lifting in order to run tests, and we can utilize that even if we do not run any tests). Test-driven cookbook design is outside of the scope of this HOWTO, but would fit nicely on top of this. This is a minimum skeleton designed to quickly get to applying configuration to an Ubuntu or CentOS virtual machine.

Quick Note on Ruby

ChefDK by default will hold its own ruby files and even wraps the gem install and other commands.


  1. Install Docker
  2. Install ChefDK
  3. Install chef-zero
  4. Install kitchen-docker
  5. Use ChefDK to generate a cookbook skeleton
  6. Add a recipe
  7. Add a template
  8. Add an attribute file
  9. Use test-kitchen to create docker containers and apply the chef cookbook

Install Docker

Centos Docker Installation

Ubuntu Docker Installation

Docker OS X Notes

  • OS X Docker Installation - docker-machine docker-machine is the replacement for boot2docker. If you already have boot2docker install that will be fine though
  • Ensure you have your shell environment variables set properly so that "docker ps" will run without error. You can simply use Docker Quickstart Terminal with the docker-machine install and it will start a terminal with this all set for you
  • Recommended: Install iterm2 if you only have the default terminal app and set Docker Quickstart Terminal to use it

Install and Configure ChefDK Environment

The ChefDK package compiles together a development environment with chef-client, knife, the chef command line tool, test-kitchen, berkshelf and drivers for vagrant. ChefInc has done the work to make sure that all the tools in the package are compatible so that you won't wind up needing to worry about json gem conflicts and become an expert in ruby dependency management just in order to converge a simple cookbook.

curl -L | sudo bash -s -- -P chefdk

Make sure you have at least version 0.2.0 (there was a bug in 0.1.0 that affected this workflow):

chef --version
Chef Development Kit Version: 0.7.0

Have a look at what Chef's environment variables settings are.

chef shell-init bash

Add them and persist in your profile

cat ~/.bash_profile
echo 'eval "$(chef shell-init bash)"' >> ~/.bash_profile
source ~/.bash_profile

Install Chef-Zero

Chef-Zero is a newer way to run with an in-memory and configuration free Chef Server that is more full featured and "real" than the previous Chef-Solo mode.

chef gem install chef-zero

Install kitchen-docker

This driver is needed to test using docker containers rather than the default vagrant

chef gem install kitchen-docker

Create a Repo

I'm going to assume a ~/chef-repo/{cookbooks,data_bags,roles,environments} structure, and we are going to follow a one-git-repo-per-cookbook model:

% mkdir ~/chef-repo/cookbooks/ntp
% cd ~/chef-repo/cookbooks/ntp
% git init .
% git touch
% git commit -m 'first commit'

Note: If you are constrained by number of repos by a Github paid account for instance you likely will not want to make a separate repo per cookbook. For this examples sake though we won't worry about that now.

Create Cookbook Skeleton

% cd ~/chef-repo/cookbooks/ntp
% chef generate cookbook .

This magically picks up the name of our cookbook based on the name of the directory we are in. It also creates:

  • metadata.rb
  • chefignore
  • Berksfile
  • .kitchen.yml
  • recipes/default.rb

It is a good idea now to make a commit so that you can go back to an unmodified skeleton if you want to:

cd ~/chef-repo/cookbooks/ntp
git add .
git commit -m 'Generated cookbook skeleton'

Add Your Recipe Code

edit recipes/default.rb and include some resources:

package "ntp" do
  action :install

template "/etc/ntp.conf" do
  source "ntp.conf.erb"
  variables( :ntp_server => "" )
  notifies :restart, "service[ntp_service]"

service "ntp_service" do
  service_name node[:ntp][:service]
  action [:enable, :start]

Add Your Template

Since we included a template resource, we need to create the content for the template. ChefDK contains a really simple generator for the 'scaffolding':

% cd ~/chef-repo/cookbooks/ntp
% chef generate template ntp.conf

All that does is create the file templates/default/ntp.conf.erb. You'll need to edit that file and include the contents that you want for ntp.conf.erb anyway (so in this case you can skip the 'chef generate template' step entirely as it only creates that file);

restrict default kod nomodify notrap nopeer noquery
restrict -6 default kod nomodify notrap nopeer noquery
restrict -6 ::1
server <%= @ntp_server %>
server     # local clock
driftfile /var/lib/ntp/drift
keys /etc/ntp/keys

Add Your Attribute File

On RHEL based systems we need to start the "ntp" service and use the "/etc/init.d/ntp" init script. On Debian/Ubuntu systems we need to start the "ntpd" service and use the "/etc/init.d/ntpd" init script. To deal with that difference correctly we included the attribute node[:ntp][:service] in the recipe and we need to set it correctly (based on the platform_family) in an attribute file.

Again you can run ChefDK to generate the attribute file or just create it directly:

% cd ~/chef-repo/cookbooks/ntp
% chef generate attribute default

That should create attributes/default.rb which you need to edit to include the content:

default[:ntp][:service] =
  case platform_family
  when "rhel", "fedora"
  when "debian"

Prepare .kitchen.yml

The file is going to be in the cookboks/ntp directory. By default it creates and tests against both centos and ubuntu containers.

TK assumes Vagrant and chef_solo so we need to make some slight changes to use docker and chef_zero.

UPDATE: It seems at some point it started defaulting to chef_zero so you may only need to update the driver entry.

Here's what the original looks like:

  name: vagrant

  name: chef_solo

  - name: ubuntu-14.04
  - name: centos-7.1

  - name: default
      - recipe[ntp::defaut]

Adjust it as follows:

  name: docker

  name: chef_zero

  - name: ubuntu-14.04
  - name: centos-7.1

  - name: default
      - recipe[ntp::defaut]

Commit Our Work To Git

Now is a good point to commit and make a checkpoint since things should be working.

git add .
git commit -m 'Added Cookbook Code'

Run Kitchen List

To see what virts ChefDK sets up to build initially we can do a kitchen list (configured in the .kitchen.yml file that ChefDK generated for us):

% kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1404  Vagrant  ChefZero     <Not Created>
default-centos-71    Vagrant  ChefZero     <Not Created>

Run a Test Kitchen Converge

The TK "converge" command will run chef-client converge and then leave the instance around to be logged into. Use "kitchen test" if you'd like to destroy the instance after you get a successful converge and not leave test containers lying around.

% cd ~/chef-repo/cookbooks/ntp
% kitchen converge default-ubuntu-1404

Login to Instance

Since you used kitchen converge previously you can login to the instance:

% cd ~/chef-repo/cookbooks/ntp
% kitchen login default-ubuntu-1404

Test CentOS

Similarly we can verify that we do the right thing for CentOS:

% cd ~/chef-repo/cookbooks/ntp
% kitchen converge default-centos-64
% kitchen login default-centos-64

Test Kitchen Matching

The instance argument for test kitchen should be read as a regular expression bounded by wildcards. So if the string you send it matches an instance it will try to converge or login to it. You can only login to a single box, so you need to give it enough to uniquely identify an instance when you're doing a login, but multiple servers can be converged with a single command line:

% kitchen converge             # converge all the instances in .kitchen.yml
% kitchen converge ubuntu      # converge all the ubuntu servers in .kitchen.yml (would match default-ubuntu-1204 and default-ubuntu-1404)
% kitchen converge centos      # converge all the centos servers in .kitchen.yml
% kitchen converge default     # converge all the default test suits in .kitchen.yml (test suites are more advanced TK use)
% kitchen login ubuntu-1204    # login to the ubuntu-1204 instance (if 'default' is the only suite you have defined)

Benefits As Starting Workflow

(This is aimed more at explaining to experienced chef users why to teach Chef in this way)

There's no explicit interaction with a Chef Server involved in this example. Under the covers, your kitchen config will be firing up a chef-zero server and using it to converge the node, but this detail is hidden and we do not need to install a chef-server first before doing work. There's no additional setup of EC2 or Digital Ocean keys as well. We also do not scribble over the User's desktop configuration. We install two utilities and get right to converging recipes as fast as possible on a virt. There is probably less initial overhead compared to setting up chef-solo and explaining dna.json and solo.rb files. The focus is as much as possible on writing Chef recipes, but done with correct tooling first.

There's no going down the wrong path. This outline conforms to the Law of Primacy -- that you will fall back on what you learn first. So we do not use chef-solo because fundamentally that approach leads to dead ends, and does not teach that chef is best used with a server. We also start the user down the path of using git-repos-per-cookbook. We use tools like test-kitchen which can grow into fully TDD design later, and we converge CentOS and Ubuntu virts on a workstation that might be MacOS (or soon Windows) because later that is the best workflow to solve that problem (rather than, say, firing up an EC2 cloud instance and logging in and using chef-zero every time you want to test a cookbook).

What Magic Just Happened

Notice that you:

  • Didn't have to configure or touch Berkshelf
  • Didn't have to find vagrant boxes or docker containers
  • Didn't have to deal with installing chef on your vagrant box or docker container
  • Didn't have to deal with old installed versions of chef on your vagrant box or docker container
  • Didn't have to use hosted chef
  • Didn't have to setup a chef server (either open source or private enterprise chef)
  • Didn't have to touch a Vagrantfile or Dockerfile
  • Didn't have to install any vagrant plugins
  • Didn't have to fight with installing ruby tools and gem dependencies

Most of the work in this HOWTO was on editing your Chef cookbook and using test kitchen to converge virts using your cookbook.

One quirk with docker testing and Chef

docker likes to control some of the networking related files like /etc/hosts more stringently so it can do some of its magic linking across containers. If you have a Chef recipe that tries to work with that file you find you get Device or resource busy errors. More details and a workaround can be found here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment