A beginners guide to Vagrant, part 2 - Provisioning and Puppet

In the first part of this begginer's guide to Vagrant, we found out how to install Vagrant and get a really basic Ubuntu box up and running. But we need something more: we need to properly set up our development environment, in a fully automated way. It's time to use provisioners to help us with these tasks. For a better understanding of how provisioners work, lets start using a very basic shell script as a provisioner.

Provisioning with Shell

The shell provisioner allows you to execute a shell script inside the vagrant box, as root. Lets use this really simple shell script as an example - it only echoes "test":

#!/bin/sh

echo "vagrant test";

Save it as script.sh in your project root folder. Now lets change our Vagrantfile and add this script as a provisioner.

Vagrant.configure("2") do |config|

  config.vm.box = "precise64"
  config.vm.box_url = "http://files.vagrantup.com/precise64.box"

  config.vm.network :private_network, ip: "192.168.33.101"

  config.vm.synced_folder "./", "/vagrant"

  config.vm.provision :shell, :path => "test.sh"

end

Run vagrant up* and you will have an output like this:

➜  vagrant-lab git:(master) ✗ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Clearing any previously set forwarded ports...
[default] Fixed port collision for 22 => 2222. Now on port 2200.
[default] Creating shared folders metadata...
[default] Clearing any previously set network interfaces...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2200 (adapter 1)
[default] Booting VM...
GuestAdditions 4.2.18 running --- OK.
[default] Waiting for machine to boot. This may take a few minutes...
[default] Machine booted and ready!
[default] Configuring and enabling network interfaces...
[default] Mounting shared folders...
[default] -- /vagrant
[default] Running provisioner: shell...
[default] Running: inline script
stdin: is not a tty
vagrant test

As you can see, the script was successfully executed, in the end of the provisioning - you can see the "vagrant test" string being echoed. For simple scripts like this, instead of writing the script in a separated file, you could also use the inline option:

config.vm.provision :shell, :inline => "echo Hello, World" 

* If your vagrant is already up, you can run a vagrant reload to "restart" it, or a vagrant provision if you only want to re-run the provisioners tasks. For turning the machine off, run vagrant halt. If you want to start from scratch, you need to run a vagrant destroy - this will destroy any changes made to the base box.

This was a very basic example showing how to add provisioners. You could do a lot of things with shell script provisioning, but we need a more consistent way for setting up our environment. Thats why we're going to use Puppet.

What is Puppet?

Puppet is a tool for automating management of infrastructure, "from provisioning and configuration to orchestration and reporting". In other words: you automate repetitive tasks, mainly for environment setup and application deployment, by defining a workflow of tasks and rules that shall be applied to a server. Puppet is an amazing tool for devOps, but it can be very complex too, specially for first-time users. In this post you will learn the basics to get started, and in the 3rd part of this series (of course, it must be trilogy) I will cover a bit more advanced stuff on puppet.

First things first

Puppet works with its own configuration language, which is written in files called "manifests". In order to keep things organised, lets create a puppet directory in our project root. Inside that, we need a manifests folder, where we are going to save our manifests. The main manifest file shall be named default.pp. So, what do we need from our virtual server? Apache, PHP 5.4 and MySQL are minimum requirements. For now, lets start with a simple task for installing Apache.

Exec { path => [ "/bin/", "/sbin/" , "/usr/bin/", "/usr/sbin/" ] }

class system-update {
  exec { 'apt-get update':
    command => 'apt-get update',
  }

  $sysPackages = [ "build-essential" ]
  package { $sysPackages:
    ensure => "installed",
    require => Exec['apt-get update'],
  }
}

class apache {
  package { "apache2":
    ensure  => present,
    require => Class["system-update"],
  }

  service { "apache2":
    ensure  => "running",
    require => Package["apache2"],
  }

}

include apache
include system-update

Manifest Overview

Lets have a quick overview of this manifest file.

Defining Paths - The first line of the manifest tells puppet where our bin folders are. If you don't define them, you will get an error like this, when running apt-get update (which is on the system-update class):

'apt-get update' is not qualified and no path was specified.
Please qualify the command or specify a path.

Classes - As you can see, we define classes and include them at the end. Its not mandatory to use classes (notice that the first Exec was not in a class), but this way you can better define dependencies. On line 18, for instance, we are requiring another whole class to be executed before running this command. We make sure that the update commands run before trying to install Apache.

Using variables - On line 8 we defined the variable $syspackages as an array for holding all system packages we want to install. Then we used it on the package directive (line 9). This is very useful for installing multiple packages with the same requirements.

A very important thing to notice is that the order in which you define your classes and include them are not necessarily the order puppet will execute the commands. The dependencies will change the order of tasks dynamically, so you have to pay attention on what must be executed first. Including the "apache" class first doesn't means that it will be executed first, since it depends on the system-update class.

Adding the puppet provisioner

Now we need to add the Puppet provisioner to our Vagrantfile, and it will be ready to run. Our Vagrantfile shall look like this:

Vagrant.configure("2") do |config|

  config.vm.box = "precise64"
  config.vm.box_url = "http://files.vagrantup.com/precise64.box"

  config.vm.network :private_network, ip: "192.168.33.101"

  config.vm.synced_folder "./", "/vagrant", id: "vagrant-root"

  config.vm.provision :puppet do |puppet|
    puppet.manifests_path = "puppet/manifests"
    puppet.options = ['--verbose']
  end

end

We basically just need to specify the path to our manifests, since we are not using modules (will talk about this later) and also we used the default nomenclature for our manifest file (default.pp).

Using your puppet

Now we must run a vagrant provision or vagrant reload (assuming the box is already started, otherwise use vagrant up) to run the puppet provisioner we just defined. The output shall be like this: puppet_provision

Now point your browser to http://192.168.33.101 (the box's ip address defined in your Vagrantfile) and you shall see the default "It worked" from Apache.

Cool, huh? But this is just a simple Apache, with no PHP at all. We need more. We can write a few more classes to install PHP and MySQL, but theres a better way for doing so.

We are going to use Puppet Modules, a really nice way for defining reusable configurations / setups. The best thing about this approach is that there are already tons of ready-to-use puppet modules on Github, making it easier for you to get the environment you want, only by including the modules in your puppet modules folder and calling them from your default manifest.

On Part III of this series (I promise it will be the last one ;P) we will cover how to work with Puppet modules for creating our awesome portable development environment, amongst other advanced aspects of Puppet.

Written by Erika Heidi on Thursday July 4, 2013
Permalink - Lang: eng - Icon: icon-doc-text - Categories: DevOps, Vagrant - Tags: vagrant, puppet, devops


comments powered by Disqus