One of the things I love about Vagrant is how it allows you to quickly create a VM that is very close to your production environment (or better yet EXACTLY like it). We’re starting to work on scaling STAGES from a single server to multiple servers so I’m using Vagrant as the test bed for this process. Because of this we need to work with multiple VMs in the same Vagrant file which isn’t covered by most tutorials.

The Base Config

I’m going to use the basic configuration we created in Getting Started With Vagrant stripped down to the very basics:

Vagrant.configure("2") do |config|
  config.vm.box = "precise64"
  config.vm.hostname = 'web'
  config.vm.box_url = "ubuntu/precise64"

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

  config.vm.provider :virtualbox do |v|
    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
    v.customize ["modifyvm", :id, "--memory", 512]
    v.customize ["modifyvm", :id, "--name", "web"]
  end
end

Then to make sure everything is working correctly:

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'precise64'...
<snip>
    default: /vagrant => /Users/scottkeckwarren/blah
$

If you’ve worked with Vagrant before you’ve seen this a MILLION times but I want to draw your attention to the “default” text that shows up over and over again.

If we don’t destroy this box before we do the next step we’ll leave it running and taking up disk space so we’re going to destroy it:

$ vagrant destroy
    default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
$ 

Again notice “default”.

And Then There Were Two

Vagrant allows us to define more virtual machines by wrapping them in blocks like the following:

config.vm.define "<vmName>" do |<vmName>|
  <snip>
end

You absolutely need to make sure you give them a unique hostname, IP address, and name setting so there aren’t conflicts. Otherwise it won’t work. At Zimco we have a Google Doc spreadsheet that keeps track of all our Vagrantfile VMs so we don’t need to worry about conflicts (that way we can have multiple projects running at the same time in case we need to jump over and fix a quick bug or eight).

For our example our initial (see below for the final) Vagrantfile looks like this:

Vagrant.configure("2") do |config|
  config.vm.define "web" do |web|
    web.vm.box = "precise64"
    web.vm.hostname = 'web'
    web.vm.box_url = "ubuntu/precise64"

    web.vm.network :private_network, ip: "192.168.56.101"

    web.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "web"]
    end
  end

  config.vm.define "db" do |db|
    db.vm.box = "precise64"
    db.vm.hostname = 'db'
    db.vm.box_url = "ubuntu/precise64"

    db.vm.network :private_network, ip: "192.168.56.102"

    db.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "db"]
    end
  end
end

This Vagrant file specifies two different VMs. One is the “web” VM and it provides web services (duh) and the other is db and it provides database services (duh again).

Now when we run vagrant up we see some very different results:

$ vagrant up
Bringing machine 'web' up with 'virtualbox' provider...
Bringing machine 'db' up with 'virtualbox' provider...
==> web: Importing base box 'precise64'...
<snip>
==> db: Importing base box 'precise64'...
<snip>
$ 

The first thing to point out here is that “default” text is gone and in it’s place is the “web” and “db” names that we defined in the Vagrantfile above. The second is that we were able to create two VMs with just a single command. I remember a couple years ago when it took hours to manually creating the VMs in VirtualBox and install Linux (sigh).

The cool thing is that vagrant automatically runs most commands on all of the VMs in the Vagrantfile:

$ vagrant status
Current machine states:

web                       running (virtualbox)
db                        running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.

$ vagrant halt
==> db: Attempting graceful shutdown of VM...
==> web: Attempting graceful shutdown of VM...
$ 

But you can also run commands on a single VM by adding the name of the VM after the command:

$ vagrant halt db
==> web: Attempting graceful shutdown of VM...
$ 

There are a few commands that will require you to enter the VMs name but Vagrant is helpful enough to tell you when that happens:

$ vagrant ssh
This command requires a specific VM name to target in a multi-VM environment.

$ vagrant ssh web
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.2.0-23-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
New release '14.04.3 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Welcome to your Vagrant-built virtual machine.
Last login: Sun Oct  4 01:46:18 2015 from 10.0.2.2
vagrant@web:~$ 

To get around this limitation (I SSH into my web server WAY more than my DB server) you can specify a default VM when you define the virtual machine:

config.vm.define "web", primary: true do |web|

Then the above command will work:

$ vagrant ssh
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.2.0-23-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
New release '14.04.3 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

Welcome to your Vagrant-built virtual machine.
Last login: Sun Oct  4 01:46:18 2015 from 10.0.2.2
vagrant@web:~$ 

Preventing some machines from booting

Let’s say you’re testing a setup with multiple web servers.

Vagrant.configure("2") do |config|
  config.vm.define "web01", primary: true do |web01|
    web01.vm.box = "precise64"
    web01.vm.hostname = 'web01'
    web01.vm.box_url = "ubuntu/trusty64"

    web01.vm.network :private_network, ip: "192.168.56.101"
    web01.vm.network :forwarded_port, guest: 22, host: 10122, id: "ssh"


    web01.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "web01"]
    end
  end

  config.vm.define "web02", autostart: false do |web02|
    web02.vm.box = "precise64"
    web02.vm.hostname = 'web02'
    web02.vm.box_url = "ubuntu/trusty64"

    web02.vm.network :private_network, ip: "192.168.56.103"
    web02.vm.network :forwarded_port, guest: 22, host: 10122, id: "ssh"


    web02.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "web02"]
    end
  end

  config.vm.define "db" do |db|
    db.vm.box = "precise64"
    db.vm.hostname = 'db'
    db.vm.box_url = "ubuntu/precise64"

    db.vm.network :private_network, ip: "192.168.56.102"
    db.vm.network :forwarded_port, guest: 22, host: 10222, id: "ssh"

    db.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "db"]
    end
  end

end

90% of the time you don’t actually need to test the load balancing piece and can run just a single web server. Normally, if you ran vagrant up it would boot all of the VMs every time so you’ll need to run vagrant up for every VM that you want to have running:

$ vagrant up web01
$ vagrant up db

This can be tedious but thankfully Vagrant has a setting that will allow you to keep VMs from auto starting:

config.vm.define "web02", autostart: false do |web02|

Then you can just run vagrant up and it will only boot those VMs that don’t have autostart: false:

$ vagrant up
Bringing machine 'web01' up with 'virtualbox' provider...
Bringing machine 'db' up with 'virtualbox' provider...
<snip>

Fixing the SSH Forwarding Issue

Note: I’m guessing this section only affects a VERY small number of setups but it’s here in case you (like me) need it.

For brevity I removed a lot of the output from when we did the vagrant up to the two VMs above. In doing so I removed a potential problem. Vagrant automatically forwards port 2222 on the host to port 22 on the VM (unless there’s a conflict). This is helpful because then you can SSH into the machine from another computer (as I do) in order to do searches and check services.

Bringing back the pieces that are important.

$ vagrant up
Bringing machine 'web' up with 'virtualbox' provider...
Bringing machine 'db' up with 'virtualbox' provider...
==> web: Importing base box 'precise64'...
<snip>
==> web: Forwarding ports...
    web: 22 => 2222 (adapter 1)
<snip>
==> db: Importing base box 'precise64'...
<snip>
==> db: Fixed port collision for 22 => 2222. Now on port 2200.
==> db: Clearing any previously set network interfaces...
==> db: Preparing network interfaces based on configuration...
    db: Adapter 1: nat
    db: Adapter 2: hostonly
==> db: Forwarding ports...
    db: 22 => 2200 (adapter 1)
<snip>
$ 

From this output we see that when we brought up the web VM it automatically took port 2222 on the host computer for the SSH forward and db took port 2200. That’s fine for most people but what if you have multiple projects running on a single host computer? Because Vagrant automatically picks the ports it’s possible they won’t match in the future.

For example what happens if we bring up db and then web:

$ vagrant up db
Bringing machine 'db' up with 'virtualbox' provider...
<snip>
==> db: Forwarding ports...
    db: 22 => 2222 (adapter 1)
$ vagrant up web
Bringing machine 'web' up with 'virtualbox' provider...
<snip>
==> web: Fixed port collision for 22 => 2222. Now on port 2200.
==> web: Clearing any previously set network interfaces...
==> web: Preparing network interfaces based on configuration...
    web: Adapter 1: nat
    web: Adapter 2: hostonly
==> web: Forwarding ports...
    web: 22 => 2200 (adapter 1)
$

Now we have opposite port assignment. If you’re like me and need these ports to always be the same (so you can consistently connect to them and don’t have to worry about looking it up every time) this is an annoyance.

To fix this we’re going to add an explicit port forward to the configuration with an id of “ssh”. This also fixes this error message that you might see:

Vagrant cannot forward the specified ports on this VM, since they
would collide with some other application that is already listening
on these ports. The forwarded port to 2222 is already in use
on the host machine.

To fix this, modify your current projects Vagrantfile to use another
port. Example, where '1234' would be replaced by a unique host port:

  config.vm.network :forwarded_port, guest: 22, host: 1234

Sometimes, Vagrant will attempt to auto-correct this for you. In this
case, Vagrant was unable to. This is usually because the guest machine
is in a state which doesn't allow modifying port forwarding.

When I forward ports I try to stick to the last octet concatenated with the port number so for our VM we’re forwarding port 22 and the IP address is 192.168.56.101 so the host port will be ‘101’ + ‘22’ = ‘10122’.

Here’s our final Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.define "web" do |web|
    web.vm.box = "precise64"
    web.vm.hostname = 'web'
    web.vm.box_url = "ubuntu/trusty64"

    web.vm.network :private_network, ip: "192.168.56.101"
    web.vm.network :forwarded_port, guest: 22, host: 10122, id: "ssh"


    web.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "web"]
    end
  end

  config.vm.define "db" do |db|
    db.vm.box = "precise64"
    db.vm.hostname = 'db'
    db.vm.box_url = "ubuntu/precise64"

    db.vm.network :private_network, ip: "192.168.56.102"
    db.vm.network :forwarded_port, guest: 22, host: 10222, id: "ssh"

    db.vm.provider :virtualbox do |v|
      v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
      v.customize ["modifyvm", :id, "--memory", 512]
      v.customize ["modifyvm", :id, "--name", "db"]
    end
  end

end

And to check it works:

$ vagrant up
Bringing machine 'web' up with 'virtualbox' provider...
Bringing machine 'db' up with 'virtualbox' provider...
<snip>
==> web: Forwarding ports...
    web: 22 => 10122 (adapter 1)
<snip>
==> db: Forwarding ports...
    db: 22 => 10222 (adapter 1)
<snip>
$ 

Now we just need to get the correct software installed and we’ll be on our way.

Have a question ask it in the comments?