Dmitry Shpika // scriptin
type Links = GitHub | LinkedIn | YouTube | StackExchange | NPM;

Minimal practical introduction to Vagrant and Chef

Published on 09 May 2013

12 mins read

First time I tried to use Vagrant with Chef (solo version) as a provisioner I got stuck with too much irrelevant information on the topic. So here is the short introduction to get you started. I wanted it to be as simple as possible, so maybe you’ll find some of my explanations a bit redundant.

Before reading this post:

Step 0: Installation

You need to install only VirtualBox and Vagrant, everything else is included in Vagrant distribution (at least it was for me). Just follow the installation instructions.

Step 1: Configure Vagrant to use Chef solo

Example of Vagrantfile contents:

Vagrant.configure("2") do |config|
  # ... other configuration options
  config.vm.provision :chef_solo do |chef| # A
    chef.cookbooks_path = "chef/cookbooks" # B
    chef.add_recipe "recipe1" # C
    chef.add_recipe "recipe2"
    chef.add_recipe "recipe3"
    # ...
  end
end

Terminology:

Step 2: Get recipes

Chef community provides you with lots of cookbooks. In case you need to install some popular software package, you’d first look there. Note: not all cookbooks work with Chef solo, and sometimes the only way to know that for sure is to try.

It’s important that you download cookbooks and keep them in you repository together with Vagrantfile. This gives you some guarantees that your project will not break when you’ll try to build it a long time after you created it, because cookbooks are likely to change over time.

Step 3: Write your own recipes

I will show you how to write simplest cookbooks. As a practical example I will use this blog’s cookbooks - see repository. If you’ll need something more than that, you’ll have to read Chef documentation.

Example 1: Installing Jekyll

Jekyll is a static blog-aware site generator written in Ruby. This blog is built on it.

Jekyll is installed as a Ruby gem and Ruby itself will be installed on VM which is created by Vagrant, because Vagrant and Chef require it to run, but there’s bunch of other programs which Jekyll uses (e.g. Git) which are not included by default (or must be updated). Thankfully, there’s a build-essential cookbook which installs or updates these tools. Download this cookbook and extract it into the directory with cookbooks (./chef/cookbooks in my example, so the cookbook must be in ./chef/cookbooks/build-essential), and add a recipe:

chef.add_recipe "build-essential"

Preparation is done, now create a directory ./chef/cookbooks/jekyll with a metadata.rb file containing cookbook metadata, which should be clear by itself:

name "jekyll"
description "Installs Jekyll"
maintainer "Dmitry Shpika"
version "0.1"

Now we need to create an actual recipe which installs Jekyll. Create a subdirectory ./chef/cookbooks/jekyll/recipes and a file default.rb in it:

gem_package "jekyll" do
  action :install
end

gem_package is a resource used to manage Ruby gems. It is in fact a Ruby function which accepts gem name as first argument ("jekyll") and Ruby block (do ... end). If you’re not familiar with Ruby, just consider blocks in a Chef cookbooks as a way to contain configuration options for certain objects (gem in this case). Inside that block we specify an action we want to perform on a gem - installation.

This is it! Just 2 files with 7 LoC.

Terminology:

Example 2: Installing Pygments and generating CSS file for syntax highlighting

Now let’s try something more involved and look at more Chef resources.

Pygments is a syntax highlighter written in Python and used by Jekyll.

First let’s create ./chef/cookbooks/pygments directory and metadata.rb in it:

name "pygments"
description "Installs python-pygments"
maintainer "Dmitry Shpika"
version "0.1"

Then, analogously to previous example, ./chef/cookbooks/pygments/recipes/default.rb:

package "python-pygments" do
  action :install
end

This installs Pygments (but this time with package resource, because this is not a gem) and we may stop here, but just for the sake of learning let’s make it so Pygments will generate CSS file with default syntax highlighting rules, but only if it’s not already there. All code snippets below must go into ./chef/cookbooks/pygments/recipes/default.rb.

If you’re not familiar with Ruby, here’s the time to seriously go and read that Just Enough Ruby for Chef article. In fact, I will go beyond that’s described there and will do my best to explain what I do, so bare with me. I’ve actually learned Ruby just before I started using Vagrant, Chef solo and Jekyll.

First thing we need is to tell Pygments where we want our CSS file to be. By default, Vagrant maps the project root directory on your system (this is where Vagrantfile is) to /vagrant directory on VM it builds. Let’s say we want this CSS to go into /vagrant/css/syntax.css. From terminal, it is just this command:

pygmentize -S default -f html > /vagrant/css/syntax.css

But Chef doesn’t know about Vagrant default directory. The way to tell it is to use json property of Chef object in Vagrantfile:

Vagrant.configure("2") do |config|
  # ...
  config.vm.provision :chef_solo do |chef|
    # ...
    chef.json = {
      "css_directory" => "/vagrant/css",
      "pygments_css_file" => "syntax.css"
    }
  end
end

To create a directory in Chef recipe, we must use directory resourse like so:

directory "/some/dir" do
  owner "root"
  group "root"
  mode 00755
  action :create
end

In our case, we better first check if directory name is actually present and only then use it:

if node["css_directory"]
  directory node["css_directory"] do
    # default owner/group permissions are OK, so we omit it
    action :create
  end
end

node is an object you can use in your recipes, it contains attributes of the system under configuration. It is called “node” because full version of Chef uses client-server architecture to configure multiple systems (“nodes”), which may be virtual or physical machines connecting to a single server.

node["some_property"] is a way to access some property we set in chef.json above. Properties can be set in different ways, but we’ll stick with that.

Next, we finally need to generate the damn CSS. But to do that we need to check if the file name is given (same as with CSS directory) and that file is not already there. Here’s the code:

if node["css_directory"] && node["pygments_css_file"] # A
  directory node["css_directory"] do
    # default owner/group permissions are OK, so we omit it
    action :create
  end

  pygments_css_file = File.join( # B
    node["css_directory"],
    node["pygments_css_file"]
  )

  execute "generate-pygments-css" do # C
    command "pygmentize -S default -f html > #{pygments_css_file}"
    not_if { File.size?(pygments_css_file) }
    action :nothing
  end

  file pygments_css_file do # D
    action :create_if_missing
    notifies :run, "execute[generate-pygments-css]", :immediately
  end
end

We’ve used two new resources: file and execute. I recommend you to read documentation about resources to understand that they do. There’s a lot of resources which you will constantly use in your recipes.

Here is the full version of ./chef/cookbooks/pygments/recipes/default.rb:

package "python-pygments" do
  action :install
end

if node["css_directory"] && node["pygments_css_file"]
  directory node["css_directory"] do
    # default owner/group permissions are OK, so we omit it
    action :create
  end

  pygments_css_file = File.join(
    node["css_directory"],
    node["pygments_css_file"]
  )

  execute "generate-pygments-css" do
    command "pygmentize -S default -f html > #{pygments_css_file}"
    not_if { File.size?(pygments_css_file) }
    action :nothing
  end

  file pygments_css_file do
    action :create_if_missing
    notifies :run, "execute[generate-pygments-css]", :immediately
  end
end

It is bigger than a previous example, but yet small and readable enough. We’re done.

Further reading