Vagrant

In this blog post, we will demonstrate how Vagrant can simplify the provisioning and configuration workflow for virtual machine environments, and look at concepts such as shared folders, networking and port forwarding as well as provisioning options.



1. Introduction


1.1 What is Vagrant?

Vagrant is suitable as interface between virtualization software (VirtualBox, Hyper-V, Docker, VMWare, AWS, ...) and configuration or automation tools such as Puppet, Chef or SaltStack.

As such Vagrant simplifies creating and maintaining portable virtual software development environments.


1.2 Why Vagrant?

  • Create new environments without impacting the underlying main operating system
  • Significantly accelerates the process of creating virtual environments by, among other things:
    • using a base image instead of reinstalling the operating system
    • configuring settings such as host name, resource allocation, network, etc.
    • being able to execute automation tools (e.g. Puppet, Chef, Ansible etc.) as soon as the basic system has been set up


1.3 Vagrant Vocabulary

  • Provider: Basic virtualization technologies used by Vagrant (VirtualBox, Hyper-V, Docker, VMWare, AWS, etc.)
  • Provisioner: Software used by Vagrant to configure and deploy VMs (Puppet, Ansible, Chef)
  • Box: Template / configuration for creating a VM with Vagrant
  • Vagrantfile:Configuration for Vagrant (type of machine(s) required for a project, how to configure and deploy these machines)


2. Installation (Debian, CentOS7/RHEL7, MacOS, Windows)

As described in the introduction, Vagrant is used to provision various virtualization software sitting on top of it. For the purpose of this blog post, we have chosen VirtualBox, which we will install as a prerequisite for Vagrant.

Below we will discuss the installation of VirtualBox and Vagrant under Linux Debian, Linux CentOS/RHEL7, MacOS and Windows.


2.1 Linux Debian

2.1.1 VirtualBox

$ sudo apt-get update
$ sudo apt-get install virtualbox


2.1.2 Vagrant

$ sudo apt-get install vagrant


2.2 Linux CentOS 7 / Red Hat Enterprise Linux 7

2.2.1 VirtualBox

$ sudo cd /etc/yum.repos.d/
$ sudo wget http://download.virtualbox.org/virtualbox/rpm/rhel/virtualbox.repo
$ sudo yum update
$ sudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ sudo yum install binutils gcc make patch libgomp glibc-headers glibc-devel kernel-headers kernel-devel dkms
$ sudo yum install VirtualBox-5.2


2.2.2 Vagrant

$ sudo yum -y install ruby
$ sudo gem install vagrant


2.3 MacOS

2.3.1 VirtualBox

 Note: For the following commands Homebrew is needed as prerequisite: https://brew.sh/index_de.

$ sudo brew cask install virtualbox


2.3.2 Vagrant

$ sudo brew cask install vagrant


2.4 Windows

2.4.1 VirtualBox

https://www.virtualbox.org/wiki/Downloads


2.4.2 Vagrant

https://www.vagrantup.com/downloads.html


3. Vagrant Basic-Commands

  • box (add, remove, etc.) boxes
  • global-status returns the status of all active Vagrant environments on the system
  • haltshuts down the running machine(s) that Vagrant manages
  • init initializes the current directory by generating an initial Vagrantfile.
  • provisionexecutes any configured provisioners against the running machines managed by Vagrant.
  • reload (equals halt && up): usually required for changes made to the Vagrantfile to take effect
  • ssh starts an SSH session in a running Vagrant machine and allows access to its shell
  • upcreates and configures guest machines in accordance to the Vagrantfile


 Note: Official documentation covering all commands of the Vagrant CLI: https://www.vagrantup.com/docs/cli/

4. The Vagrantfile

The Vagrantfile describes among other things the machine type required for a project, as well as settings / instructions concerning the configuration and provisioning of these machines.

As a rule, one Vagrantfile is created for each project, which should be versioned - e.g. via GiT. This enables other developers involved in the project to read out the code, execute Vagrant and thus already have the project up-and-running.

The syntax of the Vagrantfile is Ruby, but knowledge of Ruby programming is not absolutely necessary in order to make changes to the Vagrantfile or to understand its structure, since these are mostly simple variable assignments.


4.1 Minimal Vagrantfile

Vagrant requires at least a definition of the image to be used for the box.


 Note: You can find a list of various Vagrant Boxes ready to be utilised here: https://app.vagrantup.com/boxes/search

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


If vagrant up is now executed in the project directory (in which the Vagrantfile is located), Vagrant will already create a box with Ubuntu as operating system.


 Note: Find the official list of all utilisable options / variables here: https://www.vagrantup.com/docs/vagrantfile/

4.2 Multi-Machine

In the simplest case, one Vagrantfile is used to define configuration and provisioning parameters for a single guest machine. Often the infrastructure of a project consists of several machines, which should be known to each other or be able to communicate.

The following is an example of a quite simple Vagrantfile, which contains descriptions for several guest machines:


Vagrant.configure("2") do |config|
    
    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
    end
    
end


4.3 Shared Folders

Often the project contains files / folders / executables, which have to be transferred to the guest machine. Of course, these could be transferred via scp after boot-up of the guest machine, for example - but a simpler and more advantageous approach is the concept of shared folders - directories which are automatically synchronized between host machine and guest machine, so that a change to a corresponding file on the host is immediately visible / effective in the guest machine.

We extend the above multi-machine Vagrantfile to share the folder on the db guest machine where the database files are stored between host and guest.


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
    end
    
end


 Note: The first path specified corresponds to the (in this case relative) path on the host machine, the second path corresponds to the absolute path on the guest machine.

4.4 Provisioning

Usually, once a guest machine has been created and started up via Vagrant, it is further provisioned, software is delivered, installed and configured, and it is ensured that the corresponding services are running.

For such purposes, Vagrant can be combined with automation tools such as Puppet, Ansible, Chef or SaltStack. In order not to go beyond the scope of this blog post, we will limit ourselves to two possibilities which Vagrant itself offers: inline-provisioning via directly stating the corresponding commands in the Vagrantfile, and provisioning by executing, for example, an appropriately prepared shell script.

We want to install a Postgres database server on the db guest machine:


4.4.1 Inline-Provisioning

For this we use the option vm.provision:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell",
            inline: "sudo apt-get update && sudo apt-get install -y postgresql"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
    end
    
end


4.4.2 Provisioning via Shell-Script

Alternatively, we can also move the commands into a shell script and execute that script in its entirety. This makes sense with more extensive commands compared to inline provisioning, especially in regards to readability and maintainability. Again the vm.provision option is used, and the relative path to the corresponding shell script is specified on the host machine:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
    end
    
end


4.5 Networking

Vagrant provides some high-level networking options, such as port forwarding, connecting to a public network, or creating a private network. These options also work as abstraction layers across multiple providers (VirtualBox, VMWare, ...).


4.5.1 Private Networks

Vagrant private networks allow you to access the guest machine via an IP address that is not publicly accessible from the global Internet.

Several guest machines within a private network can communicate with each other, which is especially advantageous for multi-machine setups.

The easiest way to create a private network is to assign a private IP address via DHCP using the vm.network option and the private_network identifier:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.network "private_network", type: "dhcp"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "private_network", type: "dhcp"
    end
    
end


Sometimes, however, it is advantageous to define the IP address statically in advance. This is also possible via vm.network and private_network identifier:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.network "private_network", ip: "10.0.2.1"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "private_network", ip: "10.0.2.2"
    end
    
end

4.5.2 Public Networks

Unlike private networks, Vagrant public networks can be publicly accessible from the global Internet.

Again, the simplest way is to assign a dynamic IP address via DHCP using the vm.network option and the public_network identifier:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.network "public_network"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "public_network"
    end
    
end

And an example using a static IP address:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.network "public_network", ip: "192.168.0.1"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "public_network", ip: "192.168.0.2"
    end
    
end


If more than one network interface is available on the host machine, you can define the desired interface to which the guest machine should build a bridge by adding a :bridge clause as follows:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.network "public_network", bridge: "en0"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "public_network", bridge: "en0"
    end
    
end


4.6 Port Forwarding

Sometimes it is necessary to redirect a port of the host machine to a corresponding port in a Vagrant guest machine. Let's say our webserver runs virtualized in a corresponding Vagrant guest machine on port 80; we will most likely want to forward port 80 of the host machine accordingly.

This also makes sense for local development environments; for example, port 8080 of the host machine could be forwarded to port 80 of the guest machine, which allows the developer to access the virtualized web server in the local browser at http://localhost:8080.

We implement the latter in our Vagrantfile from above:


Vagrant.configure("2") do |config|

    # database server
    config.vm.define "db" do |db|
        db.vm.box = "ubuntu/bionic64"
        db.vm.synced_folder "./data/", "/usr/local/pgsql/data/"
        db.vm.provision "shell", path: "provision-db.sh"
    end
    
    # web server
    config.vm.define "web" do |web|    
        web.vm.box = "ubuntu/bionic64"
        web.vm.network "forwarded_port", guest: 80, host: 8081
    end
    
end


 Note: If a firewall is activated in the guest machine, you have to consider that the corresponding firewall rules, depending on the port, have to be set/adjusted accordingly.

Comments

No comments yet

Feel free to comment!