Vagrant is an ingenious tool for standardizing virtual machine configuration. Inventor Mitchell Hashimoto introduces the tool succinctly in The Tao of Vagrant. Let’s explore the power of Vagrant from the point of view of a developer, including legacy project maintenance.

Using a virtual machine to emulate an old environment

Before discussing Vagrant, consider the utility of a basic virtual machine (VM) for dealing with legacy projects. Imagine, for example, trying to get a Drupal 6 site running on your newish laptop. This hypothetical site relies on PHP 5.2, whereas the current stable version of PHP is 5.6.x.

Attempting to coerce your laptop to act like it’s 2008 (when D6 was released) may involve bloating your computer with old software, likely resulting in a setup that is subtly and fatally different from the site’s actual live environment (e.g. Ubuntu 12.04 LTS).

Drupal 6 released in 2008

Of course, this is where virtualization software like VirtualBox comes in handy. You can emulate Ubuntu 12.04 by fetching the appropriate ISO and setting up a VM using VirtualBox. Then it’s simply a matter of installing and configuring Apache, MySQL and PHP (i.e. a classic LAMP server).

This approach is satisfactory, but in practice requires enough effort that you won’t want to do it again soon. By contrast, Vagrant offers a reproducible, reliable and shareable methodology.

Introducing Vagrant

Install Vagrant and VirtualBox. On my OSX environment, I used Homebrew:

brew install Caskroom/cask/virtualbox
brew install Caskroom/cask/vagrant

Otherwise, you can consult the Vagrant download page and the VirtualBox download page respectively. Once installed, run:

mkdir ubuntu
cd ubuntu
vagrant init ubuntu/precise64

You should see output like this:

A `Vagrantfile` has been placed in this directory. You are now ready to `vagrant up` your first virtual environment! Please read the comments in the Vagrantfile as well as documentation on `vagrantup.com` for more information on using Vagrant.

Excluding comments, the new Vagrantfile contains:

Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu/precise64"
end

Note that ubuntu/precise64 is a reference to 64-bit Ubuntu 12.04 LTS (codenamed Precise Pangolin), one of the many available Vagrant boxes.

Next, let’s launch the VM.

vagrant up

On the first execution, vagrant up will download the “box” (i.e. disk image) and cache it for future reference in ~/.vagrant.d/boxes. The box size in this case is just 407M (a bargain compared to a 670M ISO image). Once the command completes, the VM is running! You can see this in two ways. First, run:

vagrant status

The output should contain:

running (virtualbox)

Secondly, open VirtualBox and see the status “Running…” indicated under the “ubuntu_…” machine.

VirtualBox running new Vagrant VM

By the way, the ubuntu part of the machine name is derived from the directory name where the Vagrantfile resides (which we created above).

Connecting to the VM

By design, the VM is bare-bones. A single port on your host machine (usually 2222) is forwarded to the VM’s port 22 to permit communication via SSH. Try it like this:

vagrant ssh

vagrant@vagrant-ubuntu-precise-64:~$

Now that we are in the VM, let’s observe a couple of interesting things. First of all, run:

ls /vagrant

You will notice that Vagrantfile is listed! This is, in fact, the very Vagrantfile you created earlier. This happens because /vagrant is a synced folder that reflects the ubuntu directory in the host machine.

Next, run:

ifconfig
eth0      Link encap:Ethernet  HWaddr 08:00:27:18:10:7f
          inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fe18:107f/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:469 errors:0 dropped:0 overruns:0 frame:0
          TX packets:315 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:49992 (49.9 KB)  TX bytes:41647 (41.6 KB)

Under eth0 (the ethernet device), find inet addr which indicates VM’s IP address (e.g 10.0.2.15).

Now, exit the VM:

exit

Back in the host machine, try reaching the VM via ping. This pings the VM exactly 4 times:

ping -c 4 10.0.2.15
PING 10.0.2.15 (10.0.2.15): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3

--- 10.0.2.15 ping statistics ---
4 packets transmitted, 0 packets received, 100.0% packet loss

The VM is unreachable because, by default, networking is configured in Network Address Translation (NAT) mode. This creates a closed network between VirtualBox and the VM. Let’s change that, because it’ll be convenient to access the D6 site directly via a browser on the host machine.

Edit the Vagrantfile so that it uses a private network (using DHCP).

Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu/precise64"
  config.vm.network "private_network", type: "dhcp"
end

To make these changes take effect, run:

vagrant reload

This reboots the machine and re-interprets the Vagrantfile. Next, log back in with SSH and re-run ifconfig:

vagrant ssh
ifconfig
eth1      Link encap:Ethernet  HWaddr 08:00:27:00:82:c4
          inet addr:172.28.128.3  Bcast:172.28.128.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fe00:82c4/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:4 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:1830 (1.8 KB)  TX bytes:2104 (2.1 KB)

Notice that there is a new virtual network card called eth1 with a new inet addr (e.g. 172.28.128.3).

Now, exit the VM:

exit

Try to ping the VM again:

ping -c 4 172.28.128.3
PING 172.28.128.3 (172.28.128.3): 56 data bytes
64 bytes from 172.28.128.3: icmp_seq=0 ttl=64 time=0.422 ms
64 bytes from 172.28.128.3: icmp_seq=1 ttl=64 time=0.339 ms
64 bytes from 172.28.128.3: icmp_seq=2 ttl=64 time=0.385 ms
64 bytes from 172.28.128.3: icmp_seq=3 ttl=64 time=0.363 ms

--- 172.28.128.3 ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss 

Success! The VM is reachable. Another way to get the VM’s IP addresses, is to run:

vagrant ssh -c "hostname -I"

Note that both addresses are returned:

10.0.2.15 172.28.128.3

Maintaining a hosts file

To avoid the inconvenience of referring to a VM by its IP address, we can associate it with a friendly name via a hosts file. A Vagrant plugin called hostmanager makes this easy:

vagrant plugin install vagrant-hostmanager

In the Vagrantfile, set ubuntu.example.com as the host name and configure the hostmanager plugin. See comments below for reference.

Vagrant.configure(2) do |config|
  # Host name of the VM
  config.vm.hostname = "ubuntu.example.com"

  # Base image for VM
  config.vm.box = "ubuntu/precise64"

  # Configure private network by DHCP
  config.vm.network "private_network", type: "dhcp"

  # Update /etc/hosts on all active VMs
  config.hostmanager.enabled = true

  # Update host machine's /etc/hosts
  config.hostmanager.manage_host = true

  # Don't ignore private IPs
  config.hostmanager.ignore_private_ip = false

  # Include offline VMs (rather than just active ones)
  config.hostmanager.include_offline = true

  # Use IP resolver to get DHCP configured address
  config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
    `vagrant ssh -c "hostname -I"`.split.last
  end
end

To update the hosts file, destroy the machine and rebuild it. You will likely be prompted for your password so that hostmanager can update your hosts file.

vagrant destroy
vagrant up

Now examine the tail of the hosts file:

tail /etc/hosts
## vagrant-hostmanager-start id: 1d6b1120-e125-4d26-82a8-559208175336
172.28.128.9	ubuntu.example.com
## vagrant-hostmanager-end

This shows that ubuntu.example.com has been successfully associated with the DHCP-assigned IP address of the VM. You can even ping it.

ping -c 4 ubuntu.example.com

Configuring a LAMP server

Check what happens if you navigate to ubuntu.example.com in your browser.

The VM without LAMP

The connection is refused because there is no webserver running. So let’s install Apache:

vagrant ssh -c "sudo apt-get -y install apache2"

Now the browser should show a more promising page:

Apache2 installed on VM

Let’s install the other elements of LAMP. Below, the DEBIAN_FRONTEND environment variable ensures that you are not prompted for anything during installation, including setting a MySQL root password. Security warning: With this method, the MySQL root password will be blank. This is handy for local development, but not desirable otherwise.

vagrant ssh -c "sudo DEBIAN_FRONTEND=noninteractive \
                apt-get -y install libapache2-mod-php5 \
                php5-mysql mysql-server"

Create a local www directory containing a dummy PHP file.

mkdir www
echo "<?php echo 'Hello from PHP!';" > www/index.php

Add the following to the Vagrantfile (before the final end):

# Sync local www directory with /var/www on the VM
config.vm.synced_folder "www", "/var/www", owner: "www-data"

This simply mounts our local www directory where Apache looks for the default website. Note that the owner of the directory (within the context of the VM) will be www-data, the Apache user. Run reload to effect the changes:

vagrant reload

Now in the browser, instead of “It works!”, you should see the greeting from PHP.

LAMP with basic PHP

Next, modify PHP script and observe that the change immediately appears in the browser (upon refresh) because the file is synced to /var/www on the VM.

echo "<?php phpinfo();" > www/index.php

LAMP with phpinfo()

Finally, download the latest 6.x version of Drupal. Extract into www, overwriting the test index.php file. The browser should show the standard Drupal installation screen.

LAMP with Drupal6

If you want to proceed with the installation, you’ll need to create a database:

MY_SQL_COMMAND="CREATE DATABASE drupal; \
                GRANT ALL ON drupal.* TO drupal@localhost \
                IDENTIFIED BY 'password';"
vagrant ssh -c "mysql -u root -e \"$MY_SQL_COMMAND\""

This creates a MySQL user called drupal and grants it all permissions on database called drupal, authenticated with an insecure password password. If you want to, you can proceed with the instructions in the browser to complete the setup of Drupal.

LAMP with Drupal6 - Installed

Refactoring into a provisioning script

In the same directory as the Vagrantfile, create a new file called provision, containing the below:

#! /bin/bash

# Exit with failure immediately if any command fails
set -e

# Install Apache
sudo apt-get -y install apache2

# Install PHP and MySQL with Apache bindings
sudo DEBIAN_FRONTEND=noninteractive \
     apt-get -y install libapache2-mod-php5 \
     php5-mysql mysql-server

# Create a dummy Drupal database
MY_SQL_COMMAND="CREATE DATABASE drupal; \
                GRANT ALL ON drupal.* TO drupal@localhost \
                IDENTIFIED BY 'password';"
mysql -u root -e "$MY_SQL_COMMAND"

# Restart Apache
/etc/init.d/apache2 restart

Add the following line to the Vagrantfile, to instruct Vagrant to execute the provision script on the VM during provisioning.

# Use shell script for provisioning
config.vm.provision "shell", inline: File.read('provision'), privileged: false

(Note that file: can be used instead of inline:, as per the shell provisioning documentation, but the latter makes debugging easier, since it forces an update with every vagrant reload --provision.)

Now, for the moment of truth, destroy and recreate your entire VM with just two commands:

vagrant destroy
vagrant up

For reference, this is the final Vagrantfile:

# vi: set ft=ruby :
Vagrant.configure(2) do |config|
  # Host name of the VM
  config.vm.hostname = "ubuntu.example.com"

  # Base image for VM
  config.vm.box = "ubuntu/precise64"

  # Configure private network by DHCP
  config.vm.network "private_network", type: "dhcp"

  # Update /etc/hosts on all active VMs
  config.hostmanager.enabled = true

  # Update host machine's /etc/hosts
  config.hostmanager.manage_host = true

  # Don't ignore private IPs
  config.hostmanager.ignore_private_ip = false

  # Include offline VMs (rather than just active ones)
  config.hostmanager.include_offline = true

  # Use IP resolver to get DHCP configured address
  config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
    `vagrant ssh -c "hostname -I"`.split.last
  end

  # Sync local www directory with /var/www on the VM
  config.vm.synced_folder "www", "/var/www", owner: "www-data"

  # Use shell script for provisioning
  config.vm.provision "shell", inline: File.read('provision'), privileged: false
end

Next steps

There’s a wealth of other boxes listed on Vagrantbox.es. For example, you can use Vagrant to fire up a Windows 8.1 box supplied by Microsoft.

# vi: set ft=ruby :
Vagrant.configure('2') do |config|
  # Supply name and URL for box
  config.vm.box = 'win81'
  config.vm.box_url = 'http://aka.ms/vagrant-win81-ie11'

  # Enable GUI
  config.vm.provider "virtualbox" do |vm|
    vm.gui = true
  end
end

Suffice to say that since Mitchell’s first Vagrant commmit in 2010, the project has very quickly become an indispensable tool in any developer’s tool belt. The project’s activity remains hot, so we can expect more great things to come.

Vagrant project activity on Github