How to Setup a Virtual Server using Vagrant

Krishna Iyer Easwaran

November 8, 2019

Vagrant from Hashicorp is a program that provides a declarative interface to create and manage Virtual Machines (VM). It defines the state of a VM and instructs VM providers (ex: VirtualBox, VMWare) how to create/update/manage it.

Why is this needed?

Creating a simple VM is relatively easy thanks to the VM providers. But managing VMs and sharing their state with other users is very complex and this is where Vagrant does it’s magic.

How Does It Work?

Vagrant In Action

Note: Vagrant doesn’t actually create virtual machines. It instructs the VM provider to create/update/delete a VM based on the configuration.

Pre-requisites

To follow along this tutorial, the following are required

Necessary Terminology

TermDescription
Virtual Machine (VM) / GuestA software emulation of a real machine.
Host (machine/OS)The environment which hosts the Virtual Machine (ex: a laptop or even a hypervisor).
VM ProviderA program such as VMWare, VirtualBox etc., that creates/manages VMs.
Vagrant BoxA base image of an Operating system (with additional packages) that’s pre-built for convenience. These are listed in the Vagrant Box Catalog.

Basic Setup

The following is a barebones Vagrantfile

Vagrant.configure("2") do |config|
    config.vm.box = "ubuntu/bionic64"
    config.vm.provider "virtualbox" do |v|
        v.memory = 2048
        v.cpus = 1
    end
end

Place this file in a folder of choice and run vagrant

$ mkdir Documents/vagrant
$ cd Documents/vagrant
$ vagrant up

This will trigger vagrant to download the box (if not already locally available), configure the VM provider and start up the VM.

Note: All the following bash commands are executed in the same directory as the vagrant file unless otherwise specified.

Vagrant also creates an SSH key pair for this machine and sets up an ssh-agent on the VM. To check the ssh config

$ vagrant ssh-config

# This is a sample output
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile <path>/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes    
  LogLevel FATAL

To ssh into the machine

$ vagrant ssh

Provisioning

One of the key features of Vagrant is declarative configuration of the VM state. This usually involves installing/updating necessary packages, configuring users and/or running custom scripts, a process dubbed provisioning.

There are multiple ways of defining the provisioning script. The following example shows the inline method.

Anything defined within the <<SCRIPT...SCRIPT block is interpreted as a shell script by Vagrant.

$basicscript= <<SCRIPT
sudo apt update
sudo apt -y dist-upgrade
sudo apt -y autoremove
SCRIPT

Vagrant.configure("2") do |config|
	config.vm.box = "ubuntu/bionic64"
    config.vm.provider "virtualbox" do |v|
	    v.memory = 2048
	    v.cpus = 1
 	end
	config.vm.provision "shell" do |s|
    	s.inline = $basicscript
    end
end

config.vm.provision defines a shell provisioner block and inline indicates that it’s an inline script.

With this Vagrantfile, Vagrant will create the VM with the specified box and once the basic setup is complete, the inline provisioning script will be executed (updating packages in this case).

Provisioning scripts are only run on the first instantiation of $ vagrant up, i.e, while creating the VM for the first time. For an existing VM, use $ vagrant provision to execute the provisioning script.

Alternatively, a separate script file can be used for more complex actions (in order to keep the Vagrantfile simple). This is done using the path option.

Vagrant.configure("2") do |config|
  ...
	config.vm.provision "shell" do |s|
      s.path = "somescript.sh"
    end
end

Vagrant copies the script file to the VM and executes it while provisioning. A remote path where the script can be fetched can also be used.

s.path = "https://example.com/somescript.sh"

Sharing Folders

Vagrant makes it easier than ever to shares files/folders between the host and the guest.

The synced_folder instruction is used for this.

config.vm.synced_folder  "path on the host" , "path on the guest"

In the following example, the contents of the host’s Documents folder can be accessed in the VM at the /Docs directory.

Vagrant.configure("2") do |config|
  ...
    config.vm.synced_folder  "~/Documents" , "/Docs"
end

It’s important to note that by default, the folder in which the Vagrantfile exists is shared with the VM at the /Vagrant directory. This can be disabled if necessary.

config.vm.synced_folder ".", "/vagrant", disabled: true

Network Configuration

In order to be able to access services on the VM externally (ex: a web server) Vagrant supports basic network configuration.

An IP address can be assigned to the VM.

config.vm.network "public_network", ip: "192.168.50.3"

If a web server is running on the VM at port 80 then it can be accessed at http://192.168.50.3.

A hostname can also be used.

config.vm.hostname = "myvm.local"
config.vm.network "public_network", ip: "192.168.50.3", hostname: true

This adds the host entry to the /etc/hosts file of the host machine.

192.168.50.3 myvm myvm.local

If a web server is now running on the VM at port 80 then it can be accessed at http://myvm.local or simply http://myvm

Vagrant also supports port forwarding

config.vm.network "forwarded_port", guest: 80, host: 8080

Multiple VMs

And finally, all the configuration options mentioned above can be replicated to create N machines using the same Vagrantfile. The define keyword is used to group options for a single VM.

$basicscript= <<SCRIPT
sudo apt update
sudo apt -y dist-upgrade
sudo apt -y autoremove
SCRIPT

$vm1script= <<SCRIPT
# Add custom commands to be run only for VM 1
SCRIPT

$vm2script= <<SCRIPT
# Add custom commands to be run only for VM 2
SCRIPT

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

    # Common settings
    config.vm.synced_folder "./", "/srv"
    config.vm.provision :shell, inline: $basicscript
    
    # Settings for VM 1
    config.vm.define "vm1" do |master| 
        vm1.vm.network "private_network", ip: "192.168.50.3"
        vm1.vm.provision "shell" do |s|
            s.inline:  $vm1script, #run the vm1script defined above
        end
    end

    # Settings for VM 2
    config.vm.define "vm2" do |minion|
        vm2.vm.network "private_network", ip: "192.168.50.2"
        vm2.vm.provision "shell" do |s|
            s.inline:  $vm2script, #run the vm2script defined above
        end
    end
end

References

  1. Official Vagrantfile reference.