Your Situation

This could be quite varied, you could be:

A solo developer looking for a fast/easy way to have a local dev environment that resembles your production environment (say you develop on OS X, but are deploying to an infrastructure running some distribution of Linux. As a bonus, there is also an easy way to deploy to Amazon’s EC2 if you have a solid setup locally. A member of a team, where everyone has their own development style and want to avoid the headaches of cross-platform support. Someone who normally sets up servers in a third-party hosting environment, but you want to test your deployment without paying a bunch of money in wasted servers (this is where I am!)

What is Vagrant?

Vagrant is essentially a wrapper around a variety of virtual machine providers. If you have ever used Make to build a piece of software, it is kind of like that except with virtual machines. It provides a single command that uniformly creates, provisions, destroys, and connects to machines. You can use many different VM providers, but I will be using VirtualBox because it is free and easy to use. Usually it is a pain in the butt to create a virtual machine, install the operating system, etc. Vagrant makes it super easy, and there are lots of premade “boxes” for you to use (more on this later).

What is Puppet?

Puppet is an infrastructure automation tool and we are going to use it to take the hard work out of setting up our systems. We can do this because lots of people have put a ton of effort into writing modules that we can use. This won’t be a tutorial on Puppet, but it will go over the basics so that you can use modules that other people have written.

But I don’t like/use Puppet!

That is fine. Thankfully Vagrant is flexible in its provisioners and you can read more about the alternatives. For simplicity I am just going to cover Bash and Puppet since that is what I am familiar with. The overall process should be the same if you decide to use Chef or Ansible, but because I don’t know much about them, I won’t discuss them further.

Getting Started

Installation

Vagrant is a breeze to install: you can read over their installation instructions. Essentially, download the package that is relevant for your platform of choice and install it in the way you would normally install a package.

You will also need to install your virtual machine provider, in my case VirtualBox.

First VM

Once it is installed, you will want to create a new project directory and initialize it.

$ mkdir ~/Vagrant $ cd ~/Vagrant $ vagrant init 1 2 3 4 $ mkdir ~ / Vagrant $ cd ~ / Vagrant $ vagrant init

This will create a new directory and get it ready with a file called Vagrantfile which will contain all the information that Vagrant needs to manage your dev environments.

In that file there will be a bunch of comments about the different things you can put in that file. For now we just care about configuring a base “box” (which is just a virtual machine image) and some other machine properties. I ended up with a file that looked something like this:

# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| # Every vagrant virtual env requires a box to build off of config.vm.box = "puppetlabs-precise64" config.vm.box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box" config.vm.hostname = "development.kloudless.vm" config.vm.network :private_network, ip: "192.168.33.10" config.vm.network :forwarded_port, guest: 80, host: 8080 # VirtualBox Specific Customization config.vm.provider :virtualbox do |vb| # Use VBoxManage to customize the VM. For example to change memory: vb.customize ["modifyvm", :id, "--memory", "1024"] end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant . configure ( "2" ) do | config | # Every vagrant virtual env requires a box to build off of config . vm . box = "puppetlabs-precise64" config . vm . box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box" config . vm . hostname = "development.kloudless.vm" config . vm . network : private_network , ip : "192.168.33.10" config . vm . network : forwarded_port , guest : 80 , host : 8080 # VirtualBox Specific Customization config . vm . provider : virtualbox do | vb | # Use VBoxManage to customize the VM. For example to change memory: vb . customize [ "modifyvm" , : id , "--memory" , "1024" ] end end

Here are the things that we configured so far:

config.vm.box : The name of the box that you are bringing up. config.vm.box_url : The location where Vagrant can look to download the box if you don’t already have a copy on your machine.The box that I chose is provided by Puppet Labs and doesn’t have any pre-installed provisioning software. This will be installed later as part of the bootstrapping process so that it can always be up to date. config.vm.hostname : The hostname of the VM. config.vm.network :private_network : The private IP address that the VM will have on the private VM network. config.vm.network :forwarded_port : The port labelled “guest” on the guest VM will be accessible on the port labelled “host” on your machine. VM memory: I gave my VM 1GB because it seemed like it would be enough (Note: this is provider dependent).

Once you have the file in place you can create and provision your VM from the same directory:

$ vagrant up 1 $ vagrant up

After that command finishes running, you will have a VM ready for you to connect and start messing around with. You can access the machine via SSH using:

$ vagrant ssh 1 $ vagrant ssh

You will now have a shell on your virtual machine as the vagrant user. The user has passwordless sudo access on the machine, it full fledged Ubuntu 12.04 LTS VM and you can do whatever you want! This is great and all, but we want to make things more automated, so you will want to exit your SSH session and get rid of the VM with:

$ vagrant destroy 1 $ vagrant destroy

Automating All the Things

As was talked about earlier, the nice thing about Vagrant is that it is really easy to offload the configuration to an automated tool, in this case Puppet. Since our VM is pretty bare bones, there is some extra work that we want to do to prepare it.

Bootstrapping

The box that I chose doesn’t by default come with puppet installed on it, this was a deliberate choice to make sure that I could use the same version I am using in production without having to change the box all the time. As such we need to do a little extra work. Preparing the machine to be puppeted is relatively straightforward and we are going to take advantage of the fact that you can use multiple provisioners on a single machine. In order to use the shell provisioner we add the following lines to our Vagrantfile before the final end:

# Enable shell provisioning to bootstrap puppet config.vm.provision :shell, :path => "bootstrap.sh" 1 2 # Enable shell provisioning to bootstrap puppet config . vm . provision : shell , : path = > "bootstrap.sh"

Then we create a file called bootstrap.sh in the same folder as our Vagrantfile that contains the following:

#!/usr/bin/env bash set -e if [ "$EUID" -ne "0" ] ; then echo "Script must be run as root." >&2 exit 1 fi if which puppet > /dev/null ; then echo "Puppet is already installed" exit 0 fi echo "Installing Puppet repo for Ubuntu 12.04 LTS" wget -qO /tmp/puppetlabs-release-precise.deb \ https://apt.puppetlabs.com/puppetlabs-release-precise.deb dpkg -i /tmp/puppetlabs-release-precise.deb rm /tmp/puppetlabs-release-precise.deb aptitude update #aptitude upgrade -y echo Installing puppet aptitude install -y puppet echo "Puppet installed!" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env bash set - e if [ "$EUID" - ne "0" ] ; then echo "Script must be run as root." > &2 exit 1 fi if which puppet > /dev/null ; then echo "Puppet is already installed" exit 0 fi echo "Installing Puppet repo for Ubuntu 12.04 LTS" wget - qO / tmp / puppetlabs - release - precise . deb \ https : //apt.puppetlabs.com/puppetlabs-release-precise.deb dpkg - i / tmp / puppetlabs - release - precise . deb rm / tmp / puppetlabs - release - precise . deb aptitude update #aptitude upgrade -y echo Installing puppet aptitude install - y puppet echo "Puppet installed!"

One important thing to notice is that the script is idempotent (meaning that it can be run multiple times without having any bad effects), this is important because we can run the provisioners without creating a new machine. Your virtual machine will now be ready to be controlled via Puppet!

Adding Puppet

Since we don’t want to install Puppet on our host machine, bring up the VM and connect to it. Puppet is already installed. We can do all of our initial Puppet configuration directly within the vm. By default, the folder containing the Vagrantfile is shared on the virtual machine in the path /vagrant and this will make a good location to store our puppet configurations. Once in that directory you will want to create a skeleton of your puppet dir:

$ mkdir -p puppet/{manifests,modules} $ touch manifests/site.pp 1 2 $ mkdir - p puppet / { manifests , modules } $ touch manifests / site . pp

From here we will want to install some puppet modules that will make setting up a basic LAMP server easy (for now we will just keep everything on the same box). There are lots of different ways to install puppet modules, but the most straightforward is using the way that is built into puppet:

$ puppet module puppetlabs-apache --modulepath \ /vagrant/puppet/modules $ puppet module install puppetlabs-mysql --modulepath \ /vagrant/puppet/modules 1 2 3 4 5 $ puppet module puppetlabs - apache -- modulepath \ / vagrant / puppet / modules $ puppet module install puppetlabs - mysql -- modulepath \ / vagrant / puppet / modules

Now we will want to actually write a puppet manifest , so we will want to create /vagrant/puppet/manifests/site.pp with the following:

node 'development.kloudless.vm' { # [1] class { 'mysql::server': # [2] config_hash => { 'root_password' => 'herpderpderp' }, } include mysql::php # [3] # Configuring apache include apache # [4] include apache::mod::php apache::vhost { $::fqdn: # [5] port => '80', docroot => '/var/www/test', require => File['/var/www/test'], } # Setting up the document root file { ['/var/www', '/var/www/test'] : # [6] ensure => directory, } file { '/var/www/test/index.php' : # [7] content => '>?php echo \'>p<Hello world!>/p<\' ?<', } # "Realize" the firewall rule Firewall <| |> # [8] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 node 'development.kloudless.vm' { # [1] class { 'mysql::server' : # [2] config_hash = > { 'root_password' = > 'herpderpderp' } , } include mysql : : php # [3] # Configuring apache include apache # [4] include apache : : mod : : php apache : : vhost { $ : : fqdn : # [5] port = > '80' , docroot = > '/var/www/test' , require = > File [ '/var/www/test' ] , } # Setting up the document root file { [ '/var/www' , '/var/www/test' ] : # [6] ensure = > directory , } file { '/var/www/test/index.php' : # [7] content = > '>?php echo \'>p<Hello world!>/p<\' ?<' , } # "Realize" the firewall rule Firewall < | | > # [8] }

This describes how the server gets configured, the basic function is setting your vm with apache, mysql, and php along with a test page. Here are some more details about the different parts:

The node definition is how we collect configuration for the machine, the label ‘development.kloudless.vm’ matches the hostname that we configured the vagrant box with. This statement uses the mysql class to install the mysql server and sets up the admin password for the db as ‘herpderpderp’. That is how the php bindings for mysql get installed Those two lines install the apache server package and mod_php respectively We want an apache vhost where we can access our basic test application. This actually creates the directories where the vhost content will live That is our application! Right now it doesn’t actually use the database, but it is a good example, we can define the contents of the file inline relatively easily this way. We will replace this later. This is a way of realizing virtual resources which in this case configures your machine’s firewall rules.

Now that we have the puppet configuration ready, we need to have Vagrant use it. This can be done easily by adding the following lines to your Vagrantfile after the shell provisioner lines:

# Enable provisioning with Puppet stand alone. config.vm.provision :puppet do |puppet| puppet.manifests_path = "puppet/manifests" puppet.manifest_file = "site.pp" puppet.module_path = "puppet/modules" puppet.options = "--verbose --debug" end 1 2 3 4 5 6 7 # Enable provisioning with Puppet stand alone. config . vm . provision : puppet do | puppet | puppet . manifests_path = "puppet/manifests" puppet . manifest_file = "site.pp" puppet . module_path = "puppet/modules" puppet . options = "--verbose --debug" end

This just tells vagrant where to find the modules we installed and the manifest we wrote. So now you are ready to vagrant up . Once your box is built, you should be able to visit http://localhost:8080 and see the output of your test page.

With a few small adjustments, you could use this to develop a full blown php app. The adjustments you would probably want to make are as follows:

Remove the index.php file block from the sites.pp file Change the webroot value in the vhost code block to be “/var/www/app” Configure a shared folder that apache can point to as the webroot. For example, if you have a folder called app that contained your application and it is in the same directory as your Vagrantfile, you would add the following line to the Vagrantfile:

config.vm.synced_folder "app", "/var/www/app"

Once those changes are made, you should be able to just run vagrant provision to update the settings and you can start dumping your project files into the app directory and you should see those changes on the vm.

Learning More

If you wanted a LAMP server to develop a php application, you don’t really need to go any further. Odds are, however, that you want to do something more than just this. If you want to run a more complicated application you can do this pretty easily if you can find a module to do it. The specific details of how you use a module depends on what it is, but the documentation is usually ok. If you want to more seriously manage your dev box with puppet, you should do some more reading so you can use the different modules people have written more easily. Here is some recommended reading :

Hiera

If you are a more experienced puppet user, you might be familiar with Hiera and want to use it with the modules/classes that you have written. It is pretty easy to do. First, change the value of puppet.options in your Vagrantfile to “–verbose –debug –hiera_config /vagrant/puppet/hiera.yaml”. Now you need to populate that file with your hiera configuration, depending on how you are using it you might end up with something like this:

--- :hierarchy: - common :backends: - yaml - puppet :yaml: :datadir: /vagrant/puppet/hieradata 1 2 3 4 5 6 7 8 9 --- : hierarchy : - common : backends : - yaml - puppet : yaml : : datadir : / vagrant / puppet / hieradata

Now you will want to make a directory called hieradata in the puppet directory. From there you can put all of your hiera variables in a file called common.yaml.

Multiple Servers

One server is great, but if you have a real production infrastructure, it most likely doesn’t consist of a single machine. Vagrant is pretty nice in that it inherently supports it, you just need to do some modifications to your Vagrantfile. If we wanted to have our database server be separate from our web server. Here is our new Vagrantfile:

# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| # All Vagrant configuration is done here. The most common configuration # options are documented and commented below. For a complete reference, # please see the online documentation at vagrantup.com. # Every Vagrant virtual environment requires a box to build off of. config.vm.box = "puppetlabs-precise64" config.vm.box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box" config.vm.define :web do |www| # [1] www.vm.hostname = "dev-www.kloudless.vm" www.vm.network :private_network, ip: "192.168.33.10" www.vm.network :forwarded_port, guest: 80, host: 8080 end config.vm.define :db do |db| # [2] db.vm.hostname = "db.kloudless.vm" db.vm.network :private_network, ip: "192.168.33.11" end # VirtualBox Specific Customization config.vm.provider :virtualbox do |vb| # Use VBoxManage to customize the VM. For example to change memory: vb.customize ["modifyvm", :id, "--memory", "512"] end # View the documentation for the provider you're using for more # information on available options. # Enable shell provisioning to bootstrap puppet config.vm.provision :shell, :path => "bootstrap.sh" # Enable provisioning with Puppet stand alone. config.vm.provision :puppet do |puppet| puppet.manifests_path = "puppet/manifests" puppet.manifest_file = "site.pp" puppet.module_path = "puppet/modules" puppet.options = "--verbose --debug" end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant . configure ( "2" ) do | config | # All Vagrant configuration is done here. The most common configuration # options are documented and commented below. For a complete reference, # please see the online documentation at vagrantup.com. # Every Vagrant virtual environment requires a box to build off of. config . vm . box = "puppetlabs-precise64" config . vm . box_url = "http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box" config . vm . define : web do | www | # [1] www . vm . hostname = "dev-www.kloudless.vm" www . vm . network : private_network , ip : "192.168.33.10" www . vm . network : forwarded_port , guest : 80 , host : 8080 end config . vm . define : db do | db | # [2] db . vm . hostname = "db.kloudless.vm" db . vm . network : private_network , ip : "192.168.33.11" end # VirtualBox Specific Customization config . vm . provider : virtualbox do | vb | # Use VBoxManage to customize the VM. For example to change memory: vb . customize [ "modifyvm" , : id , "--memory" , "512" ] end # View the documentation for the provider you're using for more # information on available options. # Enable shell provisioning to bootstrap puppet config . vm . provision : shell , : path = > "bootstrap.sh" # Enable provisioning with Puppet stand alone. config . vm . provision : puppet do | puppet | puppet . manifests_path = "puppet/manifests" puppet . manifest_file = "site.pp" puppet . module_path = "puppet/modules" puppet . options = "--verbose --debug" end end

The primary change are the blocks labelled [1] and [2], those are just adding the host specific configurations for the network settings, so that they can be dealt with separately and talk to each other. Now that there are two boxes vagrant can refer to, for example if you wanted to just bring up the webserver you would do vagrant up web. The name you refer it to is the key that is the argument to config.vm.define .

In order to have puppet provision both of the servers, we also need to modify the site.pp file so we take into account the fact that we have two nodes. This is pretty straightforward and ends up essentially splitting the single node declaration into two, resulting in:

node 'dev-www.kloudless.vm' { # Configuring apache include apache include apache::mod::php apache::vhost { $::fqdn: port => '80', docroot => '/var/www/test', require => File['/var/www/test'], } # Setting up the document root file { ['/var/www', '/var/www/test'] : ensure => directory, } file { '/var/www/test/index.php' : content => '>?php echo \'>p<Hello world!>/p<\' ?<', } # "Realize" the firewall rule Firewall <| |> } node 'db.kloudless.vm' { class { 'mysql::server': config_hash => { 'root_password' => 'herpderpderp' }, } include mysql::php # "Realize" the firewall rule Firewall <| |> } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 node 'dev-www.kloudless.vm' { # Configuring apache include apache include apache : : mod : : php apache : : vhost { $ : : fqdn : port = > '80' , docroot = > '/var/www/test' , require = > File [ '/var/www/test' ] , } # Setting up the document root file { [ '/var/www' , '/var/www/test' ] : ensure = > directory , } file { '/var/www/test/index.php' : content = > '>?php echo \'>p<Hello world!>/p<\' ?<' , } # "Realize" the firewall rule Firewall < | | > } node 'db.kloudless.vm' { class { 'mysql::server' : config_hash = > { 'root_password' = > 'herpderpderp' } , } include mysql : : php # "Realize" the firewall rule Firewall < | | > }

Essentially all that happened, is that we split the original node definitions into two separate ones. Once you have these manifests in place, running vagrant up brings up both virtual machines in sequence. Once they are up, they can communicate over the private network via the configured ip addresses. The hostnames you configure can’t get resolved (it wouldn’t be too hard to put the ip’s and hostnames in each server’s /etc/hosts file through puppet, but that isn’t too relevant here). Now this is more like something you would see in your actual infrastructure.

Moving to the Cloud

Once you have a real application developed and configured you probably want it to be accessible to everyone, so why not push it out to Amazon’s EC2! This can be done easily through the AWS provider add on to Vagrant. This will basically be a different Vagrantfile that you will use specifically to push to EC2. In order to replicate the configuration we had locally, you first need to install the plugin:

$ vagrant plugin install vagrant-aws 1 $ vagrant plugin install vagrant - aws

Once you have the plugin installed, we are going to take advice from the plugin’s docs to get started quickly using a dummy box. All this means is that our configuration will be explicit within the declaration of the box. To register the dummy box, we do the following:

$ vagrant box add dummy https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box 1 $ vagrant box add dummy https : //github.com/mitchellh/vagrant-aws/raw/master/dummy.box

Then we need to modify our existing Vagrantfile to create and provision the box, since it is going to be pretty different, we can create the different configuration blocks:

# Begin the AWS Provider Configuration: config.vm.provider :aws do |aws,override| aws.access_key_id = "YOUR KEY" aws.secret_access_key = "YOUR SECRET KEY" aws.keypair_name = "KEYPAIR NAME" aws.region = "us-west-2" aws.ami = "ami-ff68f8cf" # Ubuntu 12.04LTS in us-west-2" override.ssh.username = "ubuntu" override.ssh.private_key_path = "PATH TO YOUR PRIVATE KEY" end # This box will be brought up in EC2 config.vm.define :web_aws do |web| web.vm.box = "dummy" web.vm.hostname = "www.kloudless.aws" # Dummy hostname end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Begin the AWS Provider Configuration: config . vm . provider : aws do | aws , override | aws . access_key_id = "YOUR KEY" aws . secret_access_key = "YOUR SECRET KEY" aws . keypair_name = "KEYPAIR NAME" aws . region = "us-west-2" aws . ami = "ami-ff68f8cf" # Ubuntu 12.04LTS in us-west-2" override . ssh . username = "ubuntu" override . ssh . private_key_path = "PATH TO YOUR PRIVATE KEY" end # This box will be brought up in EC2 config . vm . define : web_aws do | web | web . vm . box = "dummy" web . vm . hostname = "www.kloudless.aws" # Dummy hostname end

These blocks can just be added into your Vagrantfile before the final end. This assumes that you already have an account and a key-pair set up, so you will need to substitute your credentials into the proper place. I have chosen the Ubuntu 12.04LTS AMI because it is easy to use and us-west-2 because it is pretty close to where I am located (it is in Oregon). Now here is the somewhat tricky bit, because of the way that EC2 works, you won’t really know the hostname of the machine before you bring it up and the network configuration options of Vagrant don’t support setting the hostname. There are a couple ways around this:

Nodeless Puppet: A pretty novel approach that is fact driven. It is interesting and probably what I would recommend if you are going to use this in a real production environment. Just bring it up and do provisioning afterwards: This is clunky, but easy and what I will do for this blog post.

So you will bring up your vm with vagrant up web_aws . The shell provisioning will go ahead just fine, but the puppet provisioning will fail. Ths is ok, we just need another puppet node definition. Basically I am going to just copy the node definition on dev-www (if you wanted to bring up the database server in EC2 it would be the same kind of process):

node 'THE NEW HOSTNAME' { # Configuring apache include apache include apache::mod::php apache::vhost { $::fqdn: port => '80', docroot => '/var/www/test', require => File['/var/www/test'], } # Setting up the document root file { ['/var/www', '/var/www/test'] : ensure => directory, } file { '/var/www/test/index.php' : content => '>?php echo \'>p<Hello world!>/p<\' ?<', } # "Realize" the firewall rule Firewall <| |> } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 node 'THE NEW HOSTNAME' { # Configuring apache include apache include apache : : mod : : php apache : : vhost { $ : : fqdn : port = > '80' , docroot = > '/var/www/test' , require = > File [ '/var/www/test' ] , } # Setting up the document root file { [ '/var/www' , '/var/www/test' ] : ensure = > directory , } file { '/var/www/test/index.php' : content = > '>?php echo \'>p<Hello world!>/p<\' ?<' , } # "Realize" the firewall rule Firewall < | | > }

You can replace THE NEW HOSTNAME with the short domain name that the node thinks it has, in my case it was ip-10-251-32-195 . Now you can actually provision your vm with vagrant provision web_aws. In order to actually view the test page, you will need to have your security groups set up properly, but if you just want to check you can vagrant ssh web_aws and then curl http://localhost and see that it works.

Extra Notes

Some things work differently when you are using the AWS provider and the main one you will notice is the shared directories. These get sync’d with rsync every time you run vagrant up, vagrant reload, or vagrant provision. You can also build a bunch more aws configuration into your box definition, so you don’t have to specify it by hand.

Since it is in EC2 you can take advantage of tags and user data. In our production puppet environment, we have an enc that decides what classes to give an instance based on its tags, but that requires a puppet master, which I didn’t really talk about (right now, vagrant just uses puppet apply).

Conclusion

Hopefully I have given you a good taste of how easy it is to put these two tools together to make your development and deployment a lot easier! Puppet is a really great tool and I highly recommend learning more so you can take full advantage of this work flow. I am still working to fully utilize all these great tools and it would be great to hear about other peoples’ experiences.