Red Green Repeat Adventures of a Spec Driven Junkie

Vagrant Multimode

After encountering the networking bug from my last post, I also discovered Vagrant’s multimode style to writing a Vagrantfile in this post.

This style basically allows describing multiple virtual machines within a single Vagrantfile. Isn’t that convenient??

Requirements

If you would like to follow along, these are the system requirements:

Offline

If you are working offline or with a limited internet connection (like tethering through a phone), preload a Linux image, such as Ubuntu Xenial. The easiest way to do this after installing Vagrant and Virtualbox is:

  • Create a Vagrantfile
  • Run vagrant up

The contents of the Vagrantfile to preload Ubuntu Xenial is:

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

After running vagrant up, vagrant will download and save the box image locally, Ubuntu Xenial 64bit in this case.

Benefits of Multimode

The way I setup multiple virtual machines in my previous article was to have a separate Vagrantfile in separate folders.

While this is easy to understand, there’s a lot of duplication between each of the file.

  • If I want to update all the virtual machine setup, I would have to update each of the files.

  • For every machine I want to create, I would create another folder with the same Vagrantfile.

In general, these would be managable for a handful of machines, but two handfuls become tedious… and more than that becomes a real pain. When I was working with just four for my article, it was getting pretty painful, I couldn’t imagine ten!

With multimode vagrant, it basically sets up each virtual machine programmatically. I only have one Vagrantfile and I can specify as multipley virtual machines as I want without additional folders or files. Vagrant manages this under the hood, whether I setup two, ten, or even 100 virtual machines.

This is an example of specifying ten virtual machines:

Vagrant.configure("2") do |config|
 config.vm.box = "ubuntux/xenial64"

  (1..10).each do |worker_count|
    config.vm.define "worker#{worker_count}" do |subconfig|
      subconfig.vm.box = "ubuntu/xenial64"
      subconfig.vm.hostname = "worker#{worker_count}"

      [TODO](can there be a provision here?)
    end
  end
end

This method is scalable and easier to manage.

Listing and Accessing

To see all the machines, type: $ vagrant status to list machine names and states.

$ vagrant status
Current machine states:

worker1                   running (virtualbox)
worker2                   running (virtualbox)
worker3                   running (virtualbox)
worker4                   running (virtualbox)
worker5                   running (virtualbox)
worker6                   running (virtualbox)
worker7                   running (virtualbox)
worker8                   running (virtualbox)
worker9                   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`.

Isn’t that beautiful??

To access any worker machine, specify its name after the command. For example, to ssh into the third worker, this would be the command:

$ vagrant ssh worker3
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-122-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud

0 packages can be updated.
0 updates are security updates.
vagrant@worker3:~$ hostname
worker3

Different Machine Types

With multimode, we can bring up multiple machines easily, but configuring specific machines for different roles, like a balance loader, database server, database fail over, etc. can become tedious if we have to manually connect to each one.

Vagrant provides the define method which basically lets you define the configuration for different machine roles.

Here is an example of specifying the IP address and hostname of a “manager” machine with ten additional machines:

Vagrant.configure("2") do |config|
 config.vm.box = "ubuntux/xenial64"

  config.vm.define "manager" do |subconfig|
    subconfig.vm.box = "ubuntu/xenial64"
    subconfig.vm.hostname = "manager"
    subconfig.vm.network :private_network

    # add any manager specific configuration here
  end

  (1..10).each do |worker_count|
    config.vm.define "worker#{worker_count}" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.hostname = "worker#{worker_count}"
      subconfig.vm.network :private_network
      # add any worker configurations here
end
end

Now when we list the machines, this is the output:

$ vagrant status
Current machine states:

manager                   running (virtualbox)
worker1                   running (virtualbox)
worker2                   running (virtualbox)
worker3                   running (virtualbox)
worker4                   running (virtualbox)
worker5                   running (virtualbox)
worker6                   running (virtualbox)
worker7                   running (virtualbox)
worker8                   running (virtualbox)
worker9                   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`.

and the manager machine can be accessed by its label:

$ vagrant ssh manager
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-122-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud

0 packages can be updated.
0 updates are security updates.
vagrant@manager:~$ hostname
manager

Tips

Useful tips I have found when working with mutlimode Vagrantfile: using global variables and the shared /vagrant folder.

Global Variables

When configuring multiple different virtual machines, using global variables to keep values consistent across machines. Storing values in a single place keep Vagrantfile clean.

Below is an example of using a common BOX_IMAGE variable to store the box image used across different virtual machines and the number of workers to create.

BOX_IMAGE = "ubuntu/xenial64"
WORKER_COUNT = 10

Vagrant.configure("2") do |config|
 config.vm.box = BOX_IMAGE

  config.vm.define "manager" do |subconfig|
    subconfig.vm.box = BOX_IMAGE
    subconfig.vm.hostname = "manager"
    subconfig.vm.network :private_network
  end

  (1..WORKER_COUNT).each do |worker_count|
    config.vm.define "worker#{worker_count}" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.hostname = "worker#{worker_count}"
      subconfig.vm.network :private_network
    end
  end

Shared Folder

All vagrant virtual machines have a folder shared with the host that is accessible by the /vagrant directory.

Multimode virtual machines all share the same directory, so the manager virtual machine’s /vagrant folder will have the same contents as worker5’s /vagrant folder.

This can be useful to share data between virtual machines, especially in provisioning.

Conclusion

With Vagrant mutlimode configuration, it is even easier to setup multiple virtual machines in a single file. Using a single command, one can make multiple virtual machines with differing configurations.

I see this tool to be the perfect way to experiment with site-level architectures: load balancers, databases with followers, etc.