One of the easiest ways to get started is by using Berkshelf but here is also where most of the problems come in - so I decided to write a blog post on it and do a presentation on my findings.

It might be easy to get berkshelf via chef-dk but that’s not much fun for few reasons: not a clear understanding how bits fit together, locked into a concrete version of berkshelf - things can start to break if updated on their own, not the latest, bleeding edge version by definition - which is not much fun.

Here it goes. To start we need latest stable ruby version (2.2.2 at the time of writing).

rvm install 2.2.2 # install latest stable ruby version at the time of writing
rvm use 2.2.2 # using installed ruby version
gem install berkshelf # install berkshelf

Now that we have berkshelf installed lets install virtualbox, vagrant and test-kitchen so that we can start writing infrastucture code.

apt-get install virtualbox -y # install oracle virtual box
wget [https://bit.ly/vagrant172](https://bit.ly/vagrant172)
dpkg -i vagrant172 # install vagrant for automating vboxes
gem install test-kitchen # installs test-kitchen gem so that berks puts it into Gemfile

Lets scaffold our cookbook called hello-infra

berks cookbook hello-infra # generate infrastructure as code layout

and pull in dependecies.

cd hello-infra
gem install bundler # install bundler gem
bundle install # install test-kitchen from Gemfile

Lets change defaults to use chef-zero instead of chef-solo and use debian instead of ubuntu. Vagrant will work faster with debian than ubuntu since it’s server distro and less stuff comes with it pre-installed.

sed -i 's/chef_solo/chef_zero/g' .kitchen.yml # use chef zero instead of solo
sed -i 's/ubuntu-12.04/debian-7.8/g' .kitchen.yml # use debian instead of ubuntu
sed -i '10d' .kitchen.yml # remove centos from platfor list to begin with

Test-kitchen comes with handy commands to see if we got the configuration right.

kitchen list # outputs list of boxes configured
kitchen diagnose # outputs the list of box properties

Lets create the box, converge and verify to make sure we have everything set up correctly and good to start writing infrastructure code.

kitchen create # creates a new virtual box > 8 min in case it has to download box from internet
kitchen converge # converges instance with recipe
kitchen verify # verify tests after box's converged, first time around will pull down and install chef

The initial .kitchen.yml file should look like this:

provisioner:
name: chef_zero
platforms:
- name: debian-7.8
suites:
name: default
run_list: [‘hello-infra’]

To demonstrate test-kitchen lets build a simple nodejs app powered by supervisor and reverse proxied by nginx iteratively using TDD cycle. Lets create our first failing infrastructure test.

describe command('node -v') do
  its(:exit_status) { should eq 0 }
end

And run tests.

kitchen verify # should output failing test

To make the test pass we need to make changes to these files.

tail -1 metadata.rb
depends 'nodejs'

tail -1 Bershelf 
cookbook 'nodejs'

cat recipes/default.rb
include_recipe 'nodejs'

Now after running kitchen converge the tests pass and nodejs is installed on server. We repeat these TDD steps for creating a simple hello-world nodejs app, installing nginx, and configuring supervisor service. The resulting file tree structure looks like this:

.
├── attributes
├── Berksfile
├── Berksfile.lock
├── CHANGELOG.md
├── chefignore
├── files
│   └── default
│       ├── default
│       └── simple.js
├── Gemfile
├── Gemfile.lock
├── libraries
├── LICENSE
├── metadata.rb
├── providers
├── README.md
├── recipes
│   └── default.rb
├── resources
├── setup.sh
├── spec
│   └── default_spec.rb
├── teardown.sh
├── templates
│   └── default
├── test
│   └── integration
│       └── default
│           ├── bats
│           │   └── autorestart.bats
│           ├── rspec
│           │   └── hello-world_spec.rb
│           └── serverspec
│               └── webserver_spec.rb
├── Thorfile
└── Vagrantfile

The contents of the test files are:

cat test/integration/default/serverspec/webserver_spec.rb
require 'serverspec'
set :backend, :exec

describe command('node -v') do
  its(:exit_status) { should eq 0 }
end

describe port(3000) do
  it { should be_listening }
end

describe port(80) do
  it { should be_listening }
end

#cat test/integration/default/bats/autorestart.bats
#!/usr/bin/env bats

@test 'should automaticaly restart hello world app' {
  run pkill node
    sleep 5
    command curl localhost:3000
}

#cat test/integration/default/rspec/hello_world_spec.rb
require 'net/http'

describe 'website' do
  it 'should send greatings' do
    endpoint = Net::HTTP.new('localhost', 80)
    response = endpoint.get('/')
    expect(response.body).to match 'Hello World'
  end
end

The contents of files are:

cat recipe/default.rb
include_recipe 'nodejs'
include_recipe 'supervisor'

cookbook_file 'simple.js' do
  path 'srv/simple.js'
end

package 'nginx'

cookbook_file 'default' do
  path '/etc/nginx/sites-available/default'
  notifies :restart, 'service[nginx]'
end

service 'nginx' do
  action [:start]
end

supervisor_service 'hello-world' do
  command 'node /srv/simple.js'
  action :enable
  autostart true
  autorestart true
end

cat files/default/simple.js
var http = require('http');
http.createServer(function(req, res) {
    res.writeHead(200);
    res.end('Hello World');
    }).listen(3000);

cat files/default/default
server {
  location / {
    proxy_pass http://127.0.0.1:3000;
  }
}

To demonstrate chefspec I’ve also added spec/default_spec.rb for recipe.

require 'chefspec'
require 'chefspec/berkshelf'

describe 'test::default' do
  let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }

  before do
    stub_command("netstat -l | grep :3000").and_return(false)
  end

  it 'installs nodejs recipe' do
    expect(chef_run).to include_recipe('nodejs')
  end

  it 'installs nginx package' do
    expect(chef_run).to install_package('nginx')
  end

  it 'enables nginx' do
    expect(chef_run).to start_service('nginx')
  end
end

Chefspec can come in handy when testing combinations of data bags and commands running on multiple platforms, however I would only use them if something’s hard or takes very long time to test using integration tests. Integration tests give the most confidence and aren’t tied to implementation - verifies that implementation fulfils the contract. This makes implemtation easy to change without breaking tests.

All code commits for this example can be found on github presentation on slidshare and screencast on youtube.