Test-driven provisioning with Chef
Chef is a killer tool for automating and testing your software stack. In this post I’d like to demonstrate our method of TDD’ing our infrastructure.
Our goal will be an installation of Ruby atop Ubuntu. Start simple and progress from there.
Required tools
- Chef et al (Berkshelf, Kitchen)
- Vagrant
Visit the ChefDK installation page for detailed information on how to get up and running. ChefDK will install chef and a host of related tools (Cookstyle, Kitchen etc).
Overview
The process should be familiar for those of you who practice TDD.
I also assume a basic understanding of Chef (recipes, cookbooks, roles etc).
- We will define a failing spec (we will be using
Inspec
) that will check for a valid Ruby installation. - We will implement a recipe to install Ruby
- We will rerun our previous failing spec and assert that it is now in the green.
I prefer to keep the recipes atomic and fulfilling a single requirement. Smaller recipes can be grouped together into larger recipes, or ran sequentially using a runlist within your Chef role.
Kitchen setup
Within your cookbook, add or modify your .kitchen.yml
file to be something
along the lines of the following.
---
driver:
name: vagrant
provisioner:
name: chef_solo
always_update_cookbooks: true
verifier:
name: inspec
platforms:
- name: ubuntu/xenial64
suites:
- name: ruby
run_list:
- recipe[mycookbook::ruby]
verifier:
inspec_tests:
- test/ruby_spec.rb
There’s a few things going on here, but at a high level, we are telling Kitchen that we intend to run out tests against a Vagrant instance. That we will be using chef-solo as the provisioner (ideal for local development), and that we will be using a basic Vagrant image of ubuntu.
Next we define the suites. We have added a singe suite which will run one recipe and then run one test against that recipe.
Writing our failing spec
test/ruby_spec.rb
control 'RUBY 2.3 AGENT/ruby runtime' do
title 'Installs ruby 2.3'
describe package('ruby2.3-dev') do
it { should be_installed }
end
describe bash('ruby -v') do
its('stdout') { should include 'ruby 2.3' }
end
end
This is our failing spec. It will continue to fail until we implement the recipe below.
Implementing the recipe to install Ruby
Installing Ruby is made easy by using the Brightbox cookbook. Simply including the default recipe and setting the versions in your attributes file are the only steps that are required.
recipes/ruby.rb
include_recipe 'brightbox-ruby::default'
attributes/default.rb
default['brightbox-ruby']['gems'] = %w(rake bundler)
default['brightbox-ruby']['install_ruby_switch'] = true
default['brightbox-ruby']['default_action'] = :install
default['brightbox-ruby']['version'] = '2.3'
Asserting success
Run the suite using
chef exec kitchen test ruby # where ruby matches the suite name
And you should now have a passing spec.
The main takeaway from this article should be that there are benefits to creating small recipes that perform a single task. If and when something goes wrong, your CI build will be able to point to the exact spec that has failed. If each spec corresponds to a recipe, then you will easily know where to start debugging.