How to Jumpstart Linux Development with Puppet and Vagrant, Part Two

431

In the first part of this tutorial, we showed how to use Vagrant to automate and manage local virtual machines for a software development environment. We defined a simple Vagrantfile to specify certain attributes for a VM to run a simple web app, and got it running using Vagrant’s command line tools. In this part of the tutorial, we’ll be using Puppet to define and automate the configuration details for our VM. This way, whenever we start up the dev environment with vagrant up, it will be set up to run our web application without any additional manual configuration.

Using Puppet to specify system configuration

Puppet is an extremely powerful tool for configuring systems, and has its own language for specifying configuration. This language works in a way that’s subtly different from many other computer languages: it’s declarative. This means that, rather than specifying a list of actions that you want the computer to take (as in a shell script), you are describing a set of resources that you want to exist on the system. Resources are the building blocks of your system’s configuration, and they fall into many types that will be familiar to any *nix user: files, packages, users, and so on.

Puppet Labs logo

Let’s examine an example specific to the goal of this tutorial: getting our VM to run a simple web application. In our case, we want the application to be run by Apache, since that’s the service that will be running the application in production. In order for our VM to run an application with Apache, it will need to have the package installed. We can describe the package to Puppet like this:

package { "apache2":
  ensure => present,
}

First, we tell Puppet what type of resource we’re describing: a package. Then we tell it the title of the resource: in this case, apache2, the name of the package we want installed. Then, we define the ensureattribute to be present. This tells Puppet that we want the package to be present on our system. We can also tell Puppet what source it should use to find a package by specifying a provider; so for instance, if we were working on a webapp using Ruby on Rails, we might tell Puppet to install the rails package using the gemprovider, which will cause it to try to load the package from RubyGems:

package { "rails":
  ensure => present,
  provider => gem,
}

Each type has its own set of providers and attributes that can be used to further refine your descriptions. For instance, you could specify version => "3.0.0" if you knew you wanted the 3.0.0 version of a package. There is extensive documentation available on Puppet’s rich type and provider system to help you learn different ways to describe a desired system state with Puppet.

Puppet’s true power is that you can describe resources to it in a generic way (I want this file to exist at that location with these contents, I want this service to be running on that port, etc.), and Puppet will configure your system to match the description regardless of the individual system you are configuring. Wherever you run it, Puppet will examine the current state of the system, compare it to the state you have described, and then do whatever it needs to in order to make the system’s configuration match what you have specified. This means that we could swap out the Vagrant box in our project for a boxfile based on a completely different system, and our Puppet provisioning will still work with only minor changes.

Using what we’ve learned about Puppet’s declarative language, let’s specify some basic setup for our development environment. This code will go into a manifest file. We’ll define ours in sample-dev-env/manifests/default.ppbecause this is the default location Vagrant searches for Puppet manifest files.

$ mkdir manifests
$ vim manifests/default.pp
exec { "apt-get update":
  path => "/usr/bin",
}
package { "apache2":
  ensure  => present,
  require => Exec["apt-get update"],
}
service { "apache2":
  ensure  => "running",
  require => Package["apache2"],
}
file { "/var/www/sample-webapp":
  ensure  => "link",
  target  => "/vagrant/sample-webapp",
  require => Package["apache2"],
  notify  => Service["apache2"],
}
$ git add manifests/default.pp
$ git commit -m "Basic Puppet manifest"

There are a few new concepts in this sample, most notably the require and notify attributes. This allows you to express dependencies between resources so that things happen in the right order. In this example, we’ve specified that the apache2service can’t be run until the apache2package has been installed, and we’ve told Puppet to notify the apache2 service when it’s linked our sample webapp’s directory into the default Apache document root. We’ve also defined an exec resource to run apt-get update before attempting to install Apache—this is just to make sure that apt is updated before it tries to install anything.

Note that the order in which Puppet will take certain steps is not defined by their order in the file, as with shell scripts; we could move these resource definition blocks around, and the result of applying the manifest would remain the same.

Putting it all together

Now that we have a simple Puppet configuration defined for provisioning the VM that Vagrant creates and manages, how do we put all the pieces together so that everything works? Fortunately, Vagrant was built from the ground up with configuration management systems like Puppet in mind, so it’s really simple. All we need to do is add this line to the VM config block in our Vagrantfile:

config.vm.provision :puppet

Since we’ve placed our manifest in the default location, Vagrant will automatically detect it and use it for provisioning our VM. This means that now, when we bring up our VM with vagrant up, it will automatically be set up to serve content from the sample-webapp directory in our dev environment. Let’s give it something to serve:

$ pwd ~/sample-dev-env
$ mkdir sample-webapp
$ vi sample-webapp/index.html

<h1>Hello, world!</h1>

Now, if you visit localhost:3000/sample-webapp in a browser on your local machine, thanks to Puppet’s provisioning and Vagrant’s port forwarding, you should see your message displayed. You can continue adding and editing files on your local machine, and the changes will be reflected instantly in what’s served by the Apache instance running on your VM. 

The power of this is obvious: Now, any developer on your project can have a local VM serving live-updated code with just these commands:

$ git clone 
 This e-mail address is being protected from spambots. You need JavaScript enabled to view it
 :your_username/your-dev-env-repo.git
$ cd your-dev-env-repo
$ git clone 
 This e-mail address is being protected from spambots. You need JavaScript enabled to view it
 :your_username/your-webapp.git
$ vagrant up

That’s it! This workflow can be integrated even more tightly by defining a git submodule within your development environment for the projects it can be used to develop. As your Puppet configuration grows more complex, you may find it useful to organize your specifications into modules and classes, or explore the rich set of Puppet modules available on the Puppet Forge, where you can find powerful building blocks for your Puppet configuration. Puppet has super awesome documentation to help you progress on your journey. Vagrant also has a rich set of documentation available, including in-depth documentation on using Puppet with Vagrant. If building this type of development environment interests you, I’ve taken the simple templates from this tutorial and made them available on Github as a starting point.