View on GitHub

emersonprado.github.io

Start page
Português

Efficient Vagrantfiles – Part 1 – The minimum

Emerson Prado - 17/05/2019

  1. TL;DR
  2. Prerequisites
  3. Nice to meet you, Vagrant
  4. Bare minimum for Vagrant to work
  5. Is that really enough?
  6. What's next?

TL;DR

If you already know Vagrant and understand what a Vagrantfile is, you might skip this part of the articles, and get going with what I've summarized below.

First, create your base directory and a file called "Vagrantfile" (spelled exactly like this, capitalized and no extension) in it. Then, fill your Vagrantfile with your VM settings, like in the commented example below:

  # Send a code block to Vagrant.configure method
  # Name the returned object - Here, we call it "config"
  Vagrant.configure("2") do |config|

    # Optionally, include settings inside the global block
    # These apply to all VMs
    config.vm.provision "shell" do |shell|
      shell.inline = 'echo "Provisioning in `date`"'
    end

    # Include virtual machine definitions with individual blocks to each VM
    # Pass VM names as an attribute - The shell commands will use these
    # Name the returned object - Here, we call it "vm"
    config.vm.define 'vm_1' do |vm|
      # Inform the specific VM settings inside its block
      vm.vm.box = "ARTACK/debian-jessie"
      vm.vm.network "private_network", ip: "192.168.1.2"
    end

    # Repeat to remaining VMs
    config.vm.define 'vm_2' do |vm|
      vm.vm.box = "ARTACK/debian-jessie"
      vm.vm.network "private_network", ip: "192.168.1.3"
    end

    config.vm.define 'vm_3' do |vm|
      vm.vm.box = "centos/7"
      vm.vm.network "private_network", ip: "192.168.1.4"
    end

  end
  

Then, play around as usual and move on to part 2.

Prerequisites

This is the required knowledge for a full understanding of this article series. In fact, you only need the very basic of:

  1. Virtualization - What is a virtual machine and what it means to allocate hardware and network resources
  2. Programming
    1. Variables
    2. Ruby arrays and hashes
    3. Ruby control statements
    4. Ruby loops (specially each)
    5. Ruby blocks (you just need a grasp)
  3. Object-oriented programming concepts

Nice to meet you, Vagrant

Vagrant is a very powerful tool which, as a helper for a virtualizer like VirtualBox, allows developers to create, configure, start, stop and erase a bunch of virtual machines, with some simple commands, from a configuration file (Vagrantfile). That makes configuration and software testing fairly easier. For those who don't know Vagrant yet, I recommend reading the intro from official docs.

This series with 4 articles shows how to work with Vagrant with several VMs, with a range of hardware, OS and software configurations, in a simpler and more efficient fashion - without repeating and/or changing Vagrantfile sections all the time.

Bare minimum for Vagrant to work

Besides intalling required software - a virtualizer like VirtualBox, Ruby, and Vagrant itself - you have to create a base directory for your project, then write a Vagrantfile, which is the general configuration file for your managed virtual environment. I also strongly recommend a versioning tool like Git.

The Vagrantfile is Vagrant environment's central piece. It tells Vagrant how the virtual machines should be created and configured.

A nice starting point is to run vagrant init inside your project's base directory. This command creates a minimum Vagrantfile, with lots of comments which teach how to write basic settings. I recommend reading the resulting file to get a good grasp. I also recommend reading the command help with vagrant init -h.

But, for really mastering the stuff, it's good to write a Vagrantfile from scratch. The bare minimum mandatory contents is:

  1. A block with Vagrant.configure("2") do |config|, where config is a configurations object. Vagrant is a Ruby module, and Vagrant.configure is a method which processes configurations inside the block.
  2. Inside the Vagrant.configure block, more blocks with config.vm.define 'name' do |vm|, where vm is the virtual machine object, with each virtual machine settings. name will be the one used in Vagrant shell commands.
  3. Inside each vm.define block, the virtual machine settings. At the very least, the box name, for Vagrant to download and “mirror”, then create the virtual machine.
  4. Optionally, global settings, inside Vagrant.configure block and outside vm.define blocks, which will affect all VMs.

Here is an example of an extremely minimalist, yet already functional, Vagrantfile:

Open your preferred editor and create a file called Vagrantfile, with contents below, in your project's root directory
Important: write "Vagrantfile" exactly like this, with this capitalization and no extension. Otherwise, Vagrant just can't find the file, and nothing will work.
  Vagrant.configure("2") do |config|
    config.vm.define 'vm_1' do |vm|
      vm.vm.box = "ARTACK/debian-jessie"
    end
  end
  

Since Vagrant is a Ruby module (always remember this statement), let's think in Ruby: by sending a block to method Vagrant.configure, it returns an object we call config. This object groups all your current project settings. From this object, we call method vm.define, which creates another object, called vm, which will contain settings specific for a virtual machine.

Note the name vm_1, in the VM object creation. This will be used in Vagrant commands.

Note also the box name: ARTACK/debian-jessie. It's one from the uncountable boxes available in Vagrant's repository. You can search suitable boxes there – check OS, hardware settings, pre-installed software, and so on – then simply copy the box name to one or more virtual machines vm.box attribute. Vagrant takes care of dowloading it. You can also create boxes of your own and reference them in this setting, but this talk goes elsewhere.

Is that really enough?

Is it? Run vagrant up from the base directory, then follow the output, which probably looks like:

  $ vagrant up
  Bringing machine 'vm_1' up with 'virtualbox' provider...
  ==> vm_1: Importing base box 'ARTACK/debian-jessie'...
  ...
  ==> vm_1: Booting VM...
  ...
  ==> vm_1: Machine booted and ready!
  ==> vm_1: Checking for guest additions in VM...
  ==> vm_1: Mounting shared folders...
      vm_1: /vagrant =>
  

Then, run vagrant ssh vm_1. That's it – you're inside the virtual machine just created:

  $ vagrant ssh vm_1
  ...
  vagrant@debian:~$
  

Play at your will. Then exit with exit (wow). If you want to stop, start again, or restart the machine, just run vagrant halt vm_1, vagrant up vm_1 or vagrant reload vm_1, respectively. If your tests wrecked the machine, or you just want to start over, no problems. Just exit from it, then erase and recreate your VM with vagrant destroy vm_1 and vagrant up vm_1. Done. Brand new virtual machine, just like nothing happened. As many times as wanted.

What's next?

Now you can include several other virtual machines, using the same or different boxes - just state the one used by each machine, and Vagrant takes care of everything - then write settings for each machine, along with global ones. Example:

Remember: this code with VMs settings goes into your Vagrantfile
  Vagrant.configure("2") do |config|
    config.vm.provision "shell" do |shell|
      shell.inline = 'echo "Provisioning in `date`"'
    end
    config.vm.define 'vm_1' do |vm|
      vm.vm.box = "ARTACK/debian-jessie"
      vm.vm.network "private_network", ip: "192.168.1.2"
    end
    config.vm.define 'vm_2' do |vm|
      vm.vm.box = "ARTACK/debian-jessie"
      vm.vm.network "private_network", ip: "192.168.1.3"
    end
    config.vm.define 'vm_3' do |vm|
      vm.vm.box = "centos/7"
      vm.vm.network "private_network", ip: "192.168.1.4"
    end
  end
  

Here, the 3 calls to vm.define create 3 virtual machines, two with the already downloaded box - Vagrant stores boxes locally - and another one with a different box, also from the repository. Inside each VM definition, an internal (that is, only accessible from the host and the project VMs) network IP setting, and a global definition for a shell command - only for demonstration purposes.

You can include many settings inside each MV object, which will affect the MV itself, or global settings, outside the vm.define blocks, which will affect all machines.

You create and start all machines with vagrant up, which will generate an output like:

  ==> vm_X: Importing base box '...'...
  ...
  ==> vm_X: Provisioning in <Date/time>
  

In the end, you can ping all VMs to verify the given IPs:

  $ for IP in 192.168.1.{2..4} ; do ping -c 1 $IP ; done
  ...
  --- 192.168.1.2 ping statistics ---
  1 packets transmitted, 1 received, 0% packet loss, time 0ms
  ...
  --- 192.168.1.3 ping statistics ---
  1 packets transmitted, 1 received, 0% packet loss, time 0ms
  ...
  --- 192.168.1.4 ping statistics ---
  1 packets transmitted, 1 received, 0% packet loss, time 0ms
  ...
  

And so on. Read the official docs and let your mind go.


Code is already getting repetitive. Time to go to part 2!