Chef For Developers part 2

I’m continuing with my plan to create a series of articles for learning Chef from a developer perspective.

Part #1 gave an intro to Chef, Chef Solo, Vagrant, and Virtualbox. I also created my first Ubuntu VM running Apache and serving up the default website.

In this article I’ll get on to creating a cookbook of my own, and evolve it whilst introducing PHP into the mix.

Creating and evolving your own cookbook

1. Cook your own book

Downloaded configuration cookbooks live in the cookbooks subdirectory; this should be left alone as you can exclude it from version control knowing that the cookbooks are remotely hosted and can be downloaded as needed.

For your own ones you need to create a new directory; the convention for this has become to use site-cookbooks, but you can use whatever name you like as far as I can tell. You just need to add a reference to that directory in the Vagrantfile:

chef.cookbooks_path = ["cookbooks", "site-cookbooks", "blahblahblah"]

Within that new subdirectory you need to have, at a minimum, a recipes subdirectory with a default.rb ruby file which defines what your recipe does. Other key subdirectories are files (exactly that: files to be referenced/copied/whatever) and templates (ruby ERB templates which can be referenced to create a new file).

To create this default structure (for a cookbook called mysite) just use the one-liner:

mkdir -p site-cookbooks/mysite/{recipes,{templates,files}/default}

You’ll need to create two new files to spin up our new website; a favicon and a flat index html file. Create something simple and put them in the files/default/ directory (or use my ones).

Now in order for them to be referenced there needs to be a default.rb in recipes:

# -- Setup the website
# create the webroot
directory "#{node.mysite.web_root}" do
    mode 0755
end

# copy in an index.html from mysite/files/default/index.html
 cookbook_file "#{node.mysite.web_root}/index.html" do
    source "index.html"
    mode 0755
 end

# copy in my usual favicon, just for the helluvit..
 cookbook_file "#{node.mysite.web_root}/favicon.ico" do
    source "favicon.ico"
    mode 0755
 end

This will create a directory for the website (the location of which needs to be defined in the chef.json section of the Vagrantfile), copy the specified files from files/default/ over, and set the permissions on them all so that the web process can access them.

You can also use the syntax:

directory node['mysite']['web_root'] do

in place of

directory "#{node.mysite.web_root}" do

So how will Apache know about this site? Better configure it with a conf file from a template; create a new file in templates/default/ called mysite.conf.erb:

<VirtualHost *:80>
  DocumentRoot <%= @params[:docroot] %>
</VirtualHost>

And then reference it from the default.rb recipe file (add to the end of the one we just created, above):

web_app "mysite" do
    # where the website will live
   docroot "#{node.mysite.web_root}"

   # apache virtualhost definition
   template "mysite.conf.erb"
end

That just calls the web_app method that exists within the Apache cookbook to create a new site called “mysite”, set the docroot to the same directory as we just created, and configure the virtual host to reference it, as configured in the ERB template.

The Vagrantfile now needs to become:

Vagrant.configure("2") do |config|
  config.vm.box = "precise32"
  config.vm.box_url = "http://files.vagrantup.com/precise32.box"
  config.vm.network :forwarded_port, guest: 80, host: 8080

  config.vm.provision :chef_solo do |chef|

    chef.json = {
      "apache" => {
        "default_site_enabled" => false
      },
      "mysite" => {
        "name" => "My AWESOME site",
        "web_root" => "/var/www/mysite"
      }
    }

    chef.cookbooks_path = ["cookbooks","site-cookbooks"]
    chef.add_recipe "apache2"
    chef.add_recipe "mysite"
  end
end

Pro tip: be careful with quotes around the value for default_site_enabled; “false” == true whereas false == false, apparently.

Make sure you’ve destroyed your existing vagrant vm and bring this new one up, a decent one-liner is:

vagrant destroy --force && vagrant up

You should see a load of references to your new cookbook in the output and hopefully once it’s finished you’ll be able to browse to http://localhost:8080 and see something as GORGEOUS as:

Salmonpink is underrated

2. Skipping the M in LAMP, Straight to the P: PHP

Referencing PHP

Configure your code to bring in PHP; a new recipe needs to be referenced as a module of Apache:

chef.add_recipe "apache2::mod_php5"

It’s probably worth mentioning that

add_recipe "apache"

actually means

add_recipe "apache::default"

As such, mod_php5 is a recipe file itself, much like default.rb is; you can find it in the Apache cookbook under cookbooks/apache2/recipes/mod_php5.rb and all it does is call the approriate package manager to install the necessary libraries.

You may find that you receive the following error after adding in that recipe reference:

apt-get -q -y install libapache2-mod-php5=5.3.10-1ubuntu3.3 returned 100, expected 0

To get around this you need to add in some simple apt-get housekeeping before any other provisioning:

config.vm.provision :shell, :inline => "apt-get clean; apt-get update"

PHPInfo

Let’s make a basic phpinfo page to show that PHP is in there and running. To do this you could create a new file and just whack in a call to phpinfo(), but I’m going to create a new template so we can pass in a page title for it to use (create your own, or just use mine):

<html>
<head>
    <title><%= @title %></title>
    .. snip..
</head>
<body>
<h1><%= @title %></h1>
<div class="description">
    <?php
    phpinfo( );
    ?>
</div>
.. snip ..
</body>
</html>

The default.rb recipe now needs a new section to create a file from the template:

# use a template to create a phpinfo page (just creating the file and passing in one variable)
template "#{node.mysite.web_root}/phpinfo.php" do
    source "testpage.php.erb"
    mode 0755
    variables ({
        :title => node.mysite.name
    })
end

Destroy, rebuild, and browse to http://localhost:8080/phpinfo.php:

A spanking new phpinfo page - wowzers!

Notice the heading and the title of the tab are set to the values passed in from the Vagrantfile.

3. Refactor the Cookbook

We can actually put the add_recipe calls inside of other recipes using include_recipe, so that the dependencies are explicit; no need to worry about forgetting to include apache in the Vagrantfile if you’re including it in your recipe itself.

Let’s make default.rb responsible for the web app itself, and make a new recipe for creating the web files; create a new webfiles.rb in recipes/mysite and move the file related stuff in there:

webfiles.rb

# -- Setup the website
# create the webroot
directory "#{node.mysite.web_root}" do
    mode 0755
end

# copy in an index.html from mysite/files/default/index.html
 cookbook_file "#{node.mysite.web_root}/index.html" do
    source "index.html"
    mode 0755
 end

# copy in my usual favicon, just for the helluvit..
 cookbook_file "#{node.mysite.web_root}/favicon.ico" do
    source "favicon.ico"
    mode 0755
 end

 # use a template to create a phpinfo page (just creating the file and passing in one variable)
template "#{node.mysite.web_root}/phpinfo.php" do
    source "testpage.php.erb"
    mode 0755
    variables ({
        :title => node.mysite.name
    })
end

default.rb now looks like

include_recipe "apache2"
include_recipe "apache2::mod_php5"

# call "web_app" from the apache recipe definition to set up a new website
web_app "mysite" do
    # where the website will live
   docroot "#{node.mysite.web_root}"

   # apache virtualhost definition
   template "mysite.conf.erb"
end

include_recipe "mysite::webfiles"

And Vagrantfile now looks like:

Vagrant.configure("2") do |config|
  config.vm.box = "precise32"
  config.vm.box_url = "http://files.vagrantup.com/precise32.box"
  config.vm.network :forwarded_port, guest: 80, host: 8080

  config.vm.provision :shell, :inline => "apt-get clean; apt-get update" 

  config.vm.provision :chef_solo do |chef|

    chef.json = {
      "apache" => {
        "default_site_enabled" => false
      },
      "mysite" => {
        "name" => "My AWESOME site",
        "web_root" => "/var/www/mysite"
      }
    }

    chef.cookbooks_path = ["cookbooks","site-cookbooks"]
    chef.add_recipe "mysite"
  end
end

The add_recipes are now include_recipes moved to default.rb, the file related stuff is in webfiles.rb and there’s an include_recipe to reference this new file:

include_recipe "mysite::webfiles"

Why the refactoring is important!

Well, refactoring is a nice, cathartic, thing to do anyway. But there’s also a specific reason for doing it here: once we move from using Chef Solo to Grown Up Chef (aka Hosted Chef) the Vagrantfile won’t be used anymore.

As such, moving the logic out of the Vagrantfile (e.g., add_recipe calls) and into our own cookbook (e.g. include_recipe calls) will allow us to use our same recipe in both Chef Solo and also Hosted Chef.

Next up

We’ll be getting stuck in to MySQL integration and evolving a slightly more dynamic recipe.

All files used in this post can be found in the associated github repo.

One thought on “Chef For Developers part 2

Leave a Reply

Your email address will not be published. Required fields are marked *