When we started Chef, we had a loose set of rules for everyone to follow and sent them on their way. We quickly realized, however, that we needed to standardize how a cookbook met quality standards before it got released. We would try to make a simple change to a cookbook, and it didn’t meet our coding standards. Or they forgot to introduce kitchen. Or they remembered, but they didn’t do anything when their kitchen broke three weeks ago. It was chaos.
Essentially our cookbooks are like any other code product: they need a build process, automated testing, and a way to release them to the outside world. Without that, you’ll have chaos and doom.
The best way I know of to do this is with rake
(
see this example on my cafe
cookbook).
rake
has several advantages:
- It’s all in one file, using a common framework that other Ruby developers use
- It easily integrates within a Chef environment using the
chef exec
commands - It integrates well into any existing pipeline or CI server
Its one disadvantage is that it can be difficult for non-ruby developers to understand, however, the benefits above
far outweighs this advantage. We’ve found that with the simple rakefile
below, most people don’t even have to touch
their rakefile and can just use it.
We use the same rakefile
for every cookbook, located in the base folder of the cookbook in a dedicated git repository
for that cookbook. Here’s an example:
task default: [:clean, :style, :test]
desc 'Removes any policy lock files present, berks lockfile, etc.'
task :clean do
%w(
Berksfile.lock
.bundle
.cache
coverage
Gemfile.lock
.kitchen
metadata.json
vendor
policies/*.lock.json
commit.txt
rspec.xml
).each { |f| FileUtils.rm_rf(Dir.glob(f)) }
end
desc 'Run foodcritic and cookstyle on this cookbook'
task style: 'style:all'
namespace :style do
# Cookstyle
begin
require 'cookstyle'
require 'rubocop/rake_task'
RuboCop::RakeTask.new(:cookstyle) do |task|
# If we are in CI mode then add formatter options
task.options.concat %w(
--require rubocop/formatter/checkstyle_formatter
--format RuboCop::Formatter::CheckstyleFormatter
-o reports/xml/checkstyle-result.xml
) if ENV['CI']
end
rescue
puts ">>> Gem load error: #{e}, omitting style:cookstyle" unless ENV['CI']
end
# Load foodcritic
begin
require 'foodcritic'
desc 'Run foodcritic style checks'
FoodCritic::Rake::LintTask.new(:foodcritic) do |task|
task.options = {
fail_tags: ['any'],
progress: true,
}
end
rescue LoadError
puts ">>> Gem load error: #{e}, omitting style:foodcritic" unless ENV['CI']
end
task all: [:cookstyle, :foodcritic]
end
desc 'Run unit and functional tests'
task test: 'test:all'
namespace :test do
begin
require 'rspec/core/rake_task'
desc 'Run ChefSpec unit tests'
RSpec::Core::RakeTask.new(:unit) do |t|
t.rspec_opts = ENV['CI'] ? '--format RspecJunitFormatter --out rspec.xml' : '--color --format progress'
t.pattern = 'test/unit/_{,/*/_}/*_spec.rb'
end
rescue
puts ">>> Gem load error: #{e}, omitting tests:unit" unless ENV['CI']
end
begin
require 'kitchen/rake_tasks'
desc 'Run kitchen integration tests'
Kitchen::RakeTasks.new
rescue StandardError => e
puts ">>> Kitchen error: #{e}, omitting #{task.name}" unless ENV['CI']
end
namespace :kitchen do
desc 'Destroys all active kitchen resources'
task :destroy do
sh 'kitchen destroy'
end
end
task all: ['test:unit', 'test:kitchen:all']
end
desc 'bumps the patch version and releases the cookbook to the supermarket'
task release: 'release:all'
namespace :release do
begin
require 'bump'
require 'bump/tasks'
desc 'tags and pushes a patch change'
task tag: ['release:bump:patch'] do
sh 'git pull'
sh 'git push'
end
rescue
puts ">>> Gem load error: #{e}, omitting release:bump*" unless ENV['CI']
end
begin
require 'stove/rake_task'
Stove::RakeTask.new
rescue
puts ">>> Gem load error: #{e}, omitting operational:tag" unless ENV['CI']
end
task all: ['release:tag', 'release:publish']
end
As I’ve said before, you don’t really need to be able to understand every line of this rakefile
in order to make good
use of it. So let’s get up to speed on that part:
Setup
Before running the rakefile
you’ll need to set up some gems:
chef gem install stove bump
These gems are used for uploading to a supermarket and bumping a version, respectively. More on that below.
Running Locally
Function | Description | Command |
---|---|---|
Lint | Ensures that code meets standards | chef exec rake -t style |
Test | Ensures that code runs and is ready to go | c hef exec rake -t test` |
We run our rake
within the Chef ruby environment, so we prepend it with chef exec
which says -run this with Chef’s
built-in ruby_. That makes everything much more consistent and easy, especially considering we’re using cookstyle and
kitchen gems here.
To run your linting, just run chef exec rake -t style
. This will run
both cookstyle and foodcritic on your cookbooks.
We’ve found both linting tools to be helpful. Cookstyle is a saner wrapper around rubocop.
Another great pro-tip on using cookstyle
is that you can automatically fix easy to fix errors by
running cookstyle -a
. That saves a ton of time.
Once you get past the linting phase, you can run unit tests and kitchen with chef exec rake -t test
. We
consider test kitchen to be
an absolutely critical aspect
of our coding process. Would you ever write code that you never ran before deploying it somewhere? If you’re not using
test kitchen, that’s exactly what you’re doing!
This rakefile
will also allow you to bump your versions automatically (release:bump:patch
) and upload to a
supermarket (release:publish
). You’ll need the stove
and bump
gems installed with chef gem install stove bump
.
Also, you’ll need to add a .stove
file to house the configuration of how to talk to the supermarket, with these
contents:
{
"username": "yourusernametosupermarket",
"key": "C:/Users/yourusername/.chef/yourusername.pem",
"no-git": "true",
"endpoint": "https://supermarket.yourcompany.com"
}
The bump and publish targets should be reserved for your CI agent most of the time.
Running with CI
When you run this with a CI server, you’ll need set the CI
environment variable to true
so your tests will report
the CI way. Then simply run the targets as you need. I’ll have a version of our Jenkinsfile
in the next post.
Why not Delivery though?
My friend Matt Stratton suggests using Chef Delivery cookbooks to do this same thing. We didn’t go in this direction for a few reasons:
- Ignorance: we don’t know delivery very well and there isn’t a community around it that can get a local build up and running quickly. Most of delivery seems to be centered around getting Chef Workflow to work, which is not something we had plans to do.
- Training: more people know rake than know delivery. So rake is the easier option
- Simplicity: while rake does leave you a bit confused as to the particulars of what you’re doing, it’s all in one file and can be easily run. The delivery stuff is in a hierarchy of folders and therefore takes more to understand.
Conclusion
Having a local cookbook build that is standard in all of our projects has become essential to our implementation of Chef
at scale. I think the Rakefile
I use above is an excellent choice for standardizing in a way that is both flexible and
simple.