
Vagrant Provisioning with Chef
Provisioning Systems with Vagrant and Chef-Zero
This is small tutorial on using Vagrant to provision a system using the Chef, popular change configuration platform released in 2009 that uses Ruby scripts called recipes to configure the state of a system. Recipes can be packaged into modular components called cookbooks.
Similar to CFEngine and Puppet, Chef uses an agent to manage the configurations on a client node. The Chef agent requires access to a Chef Server to fetch cookbooks and other data, and report back on the state of the client node.
Running recipes and cookbooks requires an active Chef Server, so this naturally makes it difficult to develop and test cookbooks. Fortunately, there’s Chef Zero that creates a small in-memory Chef Server that the agent uses for coordination.
Prerequisites
Vagrant and Virtualbox are required for this to this tutorial, and ChefDK is recommended but not required. The tutorial examples use Curl and Bash, so if you use something alternative, let’s say PowerShell, you’ll have to convert any commands to the equivalent in PowerShell commands.

I have written some previous guides on how to install Vagrant, Virtualbox, and optionally ChefDK on macOS, Windows, and Linux.
Windows 8.1
Using Chocolatey to install the requirements:
macOS High Sierra 10.3
Using Homebrew to install the requirements:
VirtualBox and Friends on macOS
VirtualBox, Vagrant, Test Kitchen, Docker Machine, Minikube
medium.com
Fedora 28
Using native package manager Dandified YUM, or in short DNF, to install requirements:
Part I: Cookbook with Intelligent Defaults
In first part of this tutorial, we’ll using Vagrant’s Chef-Zero provisioner to apply a small cookbook that has some baked in intelligent defaults for running on Ubuntu 16.04 (Xenial Xerus).
Staging Work Area Structure
First let’s create a common work area for this tutorial. In bash, we can run this to create the initial structure:
mkdir -p ~/vagrant-chef/{cookbooks,nodes}
touch ~/vagrant-chef/Vagrantfile
cd ~/vagrant-chef/cookbooks
If you have ChefDK or the chef
command available on the host, you can create the cookbook structure with this command:
chef generate cookbook hello_web
chef generate attribute hello_web default
chef generate file hello_web index.html
If you do not have the chef
executable available on the host, we can manually create the cookbook structure with this:
mkdir -p hello_web/{attributes,files/default,recipes}
touch {attributes,recipes}/default.rb
Create the HTML content we want to use later:
cat <<-'HTML' > hello_web/files/default/index.html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
HTML
The resulting structure will look like this (with more files if the chef generate
command was used):
~/vagrant-chef/
├── Vagrantfile
├── cookbooks
│ └── hello_web
│ ├── attributes
│ │ └── default.rb
│ ├── files
│ │ └── default
│ │ └── index.html
│ └── recipes
│ └── default.rb
└── nodes
Finally, return to the root of the staging area:
cd ~/vagrant-chef
Vagrant Configuration
As mentioned, in this we’ll use the chef-zero
provisioner, which allows us to run recipes and cookbooks locally without the need for a full online Chef Server.
Update the Vagrantfile
with this content:
Vagrant.configure('2') do |config|
config.vm.box = 'bento/ubuntu-16.04'
config.vm.network 'forwarded_port', guest: 80, host: 8082
####### Provision #######
config.vm.provision 'chef_zero' do |chef|
chef.cookbooks_path = "cookbooks" # ❶
chef.add_recipe 'hello_web' # ❷
chef.nodes_path = 'nodes' # ❸
end
end
This provisioner is doing three things in this Vagrantfile
:
- Specify a path to where chef-zero can find the cookbooks.
- Determine what cookbooks we want to run, which is
hello_web
. - Specify a local directory that Chef Zero uses to create node json configuration files. These are stored in this directory, rather than on a Chef Server.
Bring up the server without provisioning, so make sure everything is fine up to this point:
vagrant up --no-provision
Creating The Cookbook
Now we need to create the corresponding hello_web
cookbook that will run a web server with our content on Ubuntu.
Cookbook Attributes
The first part is that we want to create attributes with some intelligent defaults. On different Linux distributions, there will be differences in in the package name, service name, and where html content is stored, called a docroot
. Here are but a few that I have discovered:

NOTE: This chart references centos/7
, but I don’t recommend this with chef_zero
provisioner for getting started, as CentOS team doesn’t include Virtualbox Guest Additions, which is required for chef_zero
. See their blog for more information if interested.
Since we are using Ubuntu, let’s stick in some defaults that make sense to Ubuntu. Edit the hello_web/attributes/default.rb
file from within the cookbooks directory:
default['hello_web']['package'] = 'apache2'
default['hello_web']['service'] = 'apache2'
default['hello_web']['docroot'] = '/var/www/html'
Cookbook Recipe
Now let’s create our cookbook recipe that will reference these values, by editing hello_web/recipes/default.rb
:
apt_update 'Update the apt cache daily' do
frequency 86_400
action :periodic
endpackage node['hello_web']['package']cookbook_file "#{node['hello_web']['docroot']}/index.html" do
source 'index.html'
action :create
endservice node['hello_web']['service'] do
supports status: true, restart: true, reload: true
action %i(enable start)
end
This cookbook recipe will configure four resources: ① to do an apt-get update
that is needed on Ubuntu, ② install the Apache web server package, ③ copy our content from a file in our cookbook, and ④ enable and start the Apache web service.
Provision the System
We can test this by simply running:
vagrant provision
Testing the Results
The results can be tested with curl:
curl -i http://127.0.0.1:8082
Which will show us:
HTTP/1.1 200 OK
Date: Sat, 11 Aug 2018 02:46:57 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Fri, 10 Aug 2018 04:24:59 GMT
ETag: "3c-5730d207145f1"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
Part II: Overriding those Intelligent Defaults
In the next part of the tutorial, we’re going to swap out Ubuntu 16.04 in favor of CentOS 7. As the intelligent defaults for Ubuntu won’t work for CentOS, we’ll have to override these.
Note that this exercise is done for illustrative purposes. Typically, if an organization does support both operating systems, you would code the cookbook to detect the operating system and reference the appropriate values. There are countless examples for more complex cookbooks to why you’d want to override attributes, e.g. such as configuring custom list of Apache modules. Overriding default attributes is a big part of the Chef experience, and is the mechanism to pass values into a cookbook.
Update the Vagrantfile
Let’s modify the Vagrantfile
to use Cent OS and set alternative attributes:
Vagrant.configure('2') do |config|
config.vm.box = 'bento/centos-7.5'
config.vm.network 'forwarded_port', guest: 80, host: 8082 ####### Provision #######
config.vm.provision 'chef_zero' do |chef|
chef.add_recipe 'hello_web'
chef.cookbooks_path = 'cookbooks'
chef.nodes_path = 'nodes' #### Override Attributes ####
chef.json = {
'hello_web' => {
'package' => 'httpd',
'service' => 'httpd',
'docroot' => '/var/www/html'
}
}
end
end
We inject a new set of attributes that make sense to Cent OS, but setting the chef.json
property to a custom ruby hash, which is converted to JSON for Chef. Chef uses both JSON and Ruby to represent attributes. For this to work, we match the hierarchy that we laid out in attributes/default.rb
.
Apply the new Vagrantfile
Let’s test the new Vagrantfile
:
vagrant destroy --force # remove previous Ubuntu system
vagrant up
This will bring up and provision the new Cent OS virtual guest.
Test the Results
The results like before can be tested with curl:
curl -i http://127.0.0.1:8082
Which will show us:
HTTP/1.1 200 OK
Date: Sat, 11 Aug 2018 04:05:43 GMT
Server: Apache/2.4.6 (CentOS)
Last-Modified: Sat, 11 Aug 2018 04:05:15 GMT
ETag: "3c-57320f7b0c9af"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html; charset=UTF-8<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
Final Thoughts
There you have it, how to run some use Vagrant to prototype and develop cookbooks using Vagrant using Chef-Zero. We only touched on a basic cookbook, but there’s so much more we could do. Because Chef Zero is a full Chef Server, we could also test roles, environments, search, and data bags.
Beyond the full Chef environment that can be tested with Chef Zero, using Vagrant, you could integrate this with other automation, such as Docker, custom shell scripts, or other change configurations solutions, whatever makes sense for your organization or learning activities.
Resources
Chef Articles
- chef-client -z: From Zero To Chef In 8.5 Seconds by John Keiser (Oct 2013)
- From Solo to Zero: Migrating to Chef Client Local Mode by Julian Dunn (Jun 2014)