Introduction to Vagrant

Vagrant is a free and open source tool developed by Hashicorp, defined as a “a tool for building and distributing development environments”. What Vagrant does is basically acting as an abstraction layer/wrapper around virtual machines providers such as Virtualbox, VMware and libvirt, allowing us to build, provision and easily replicate virtual machines environments on different operating systems, using a common syntax.

In this tutorial we learn how to install Vagrant on some of the most used Linux distributions, and the basic concepts behind its usage.

In this tutorial you will learn:

  • How to install Vagrant
  • How to initialize an environment and write a Vagrantfile
  • How to map directories and ports between host and guests
  • How to configure provisioning
  • How to manage environments and boxes
Introduction to Vagrant
Introduction to Vagrant
Category Requirements, Conventions or Software Version Used
System Distribution-agnostic
Software Vagrant and the vagrant-libvirt plugin
Other Administrative privileges.
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command $ – requires given linux-commands to be executed as a regular non-privileged user

Installation

Vagrant source code is released under the MIT license; the the tool is packaged in the official repositories of all the most used Linux distributions, and can easily be installed using their respective package managers. The native Linux virtualization stack is composed by the KVM (Kernel-based Virtual Machine) module, which let us use Linux as a type-1 level hypervisor, Qemu (a type-2 hypervisor), and libvirt, a library providing virtualization API. Vagrant support for libvirt is provided via a separate plugin. In the table below you can see distribution-specific instructions to install it together with Vagrant itself:

Distribution Command
Fedora/Fedora-based distributions
$ sudo dnf install vagrant vagrant-libvirt
Debian/Debian-based distributions
$ sudo apt install vagrant vagrant-libvirt
Archlinux
$ sudo pacman -S vagrant vagrant-libvirt

Initializing an environment

Once Vagrant and the libvirt plugin are installed on our system, we can initialize our first environment. Each virtual environment is based on a “box”, while instructions to build and configured it are specified in a configuration file which must be named “Vagrantfile”. The environment is initialized with the vagrant init command.



Let’s suppose we want to create an environment based on Fedora 37 (the latest version of the distribution at the moment of writing). We move into the directory we want to use for our project, and issue the following command:

$ vagrant init generic/fedora37

The argument we passed to the command above is the box we want to base our environment on, in this case: “generic/fedora37” (available boxes can be found on Vagrant cloud). The command generates a Vagrantfile containing basic instructions to build the environment. Let’s take a look at it.

The Vagrantfile

If we open the file generated by the command we used in the previous example, we can see the entire (uncommented) content is the following:

Vagrant.configure("2") do |config| 
  config.vm.box = "generic/fedora37" 
end

Inside Vagrantfiles we use the Ruby syntax. The snippet above creates a configuration instance and uses “config” to reference it. The “2” used in the snippet specifies the Vagrantfile API version. The box to be used as the base for the environment is provided as value to the vm.box property.



This is the bare minimum configuration we need to initialize our environment, however, there are many other aspects of the environment which can be configured in the Vagrantfile. Let’s see some examples.

Specifying resources allocated to a virtual machine

We can use the Vagrantfile to specify the resources allocated to a virtual machine, such as the number of cpus and memory. Those configuration are provider-specific; since we are using the “libvirt” provider, to allocate 2 virtual cpus and 1024 MB of memory to the environment, we would write:

config.vm.provider "libvirt" do |libvirt|
  libvirt.cpus = 2
  libvirt.memory = 1024
end

Mapping an host directory to the guest system

Inside the Vagrantifile we can specify an host directory to share with the guest system. There are several methods we can use to make the host directory accessible on the guest, depending on the available services. We, can use NFS for example, or Samba, or, as a last resort, rsync. When the latter is used, the synchronization is one-way only (from the host to the guest) and is performed only when the system is initialized or reloaded, or explicitly invoked with the vagrant rsync or vagrant rsync-auto commands. In the example below we use rsync to synchronize the /opt host directory to the /vagrant guest directory (the destination is automatically created if it doesn’t exist):

config.vm.synced_folder "/opt", "/vagrant", type: "rsync"

Mapping ports

It is possible to map host ports to guest ports, so that for example, if a web server on the guest system is listening on port 80, we can access it directly from the host system, using whatever port we decide to map to it. In the following example, we map guest port 80 to host port 8080:

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

Configure provisioning

Vagrant supports different methods to provision virtual machines, among the others: “Shell”, “Ansible”, “Chef”, “File” and “Docker”. The “Shell” method is the most simple and involves the use of standard commands and shell logic, which can be specified “inline” or in a dedicated, executable script. Here is an example of an inline setup:

config.vm.provision "shell", inline <<-SHELL
  dnf install -y httpd
  systemctl enable --now httpd
SHELL



In the example above a Ruby “heredoc” construct is used to define a multi-line string, preserving indentation. The “heredoc” starts with the <<- symbol and uses “SHELL” as delimiter. If we decide to use a dedicated script, we must point to its path using the following syntax:

config.vm.provision "shell", path: "bootstrap.sh"

If the path is not absolute, is considered relative to the position of the Vagrantfile.

Another provisioning method we can use with Vagrant is Ansible. Two strategies can be used: “ansible” and “ansible_local”. In both cases the ansible-playbook command is used to execute tasks specified in a playbook. What changes is where the command is executed and where the playbooks are looked for: on the host and on the guest system, respectively. In the example below we provision the machine by executing a playbook called “playbook.yml” on the host machine:

config.vm.provision "ansible" do |ansible|
  ansible.playbook = "playbook.yml"
end

At this point you may wonder how can you target the guest machine from a playbook. Vagrant automatically creates the .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory inventory file in your project directory when Ansible is used as a provisioner, creating entries for the machines it manages. In this case, for instance, since the name of the machine is “default”, the related entry in the inventory will be something like:

 Generated by Vagrant

default ansible_host=192.168.121.124 ansible_port=22 ansible_user='vagrant' ansible_ssh_private_key_file='/home/doc/vagrant/.vagrant/machines/default/libvirt/private_key'

We can further configure how Ansible is executed. We can, for example, pass extra variables as we do with the -e option when we invoke the ansible-playbook command manually, via “extra_vars”. We can either pass the path of a YAML file containing the variables, or use an hash directly, like we did in the example below:

config.vm.provision "ansible" do |ansible|
  ansible.playbook = "playbook.yml"
  ansible.extra_vars = {
    packages: ['httpd', 'vim']
  }
end

You can find more about Vagrant provisioning providers on the dedicated section of the Vagrant documentation.

Building and accessing the virtual machine

Once a Vagrantfile is ready, to build the environment all we have to do is to run the following command, in the same directory where the Vagrantfile is:

$ sudo vagrant up



The command will build the environment using the instructions provided in the Vagrantfile. Once the virtual machine is running, to login into it via ssh we can run:

$ sudo vagrant ssh

We will log in as the “vagrant” user, which is able to run sudo without providing a password.

Getting the status of a virtual machine

To check the status of a virtual machine with Vagrant we can run:

$ sudo vagrant status

In this case, since the machine is running, we receive the following output:

Current machine states:

default                   running (libvirt)

The Libvirt domain is running. To stop this machine, you can run
`vagrant halt`. To destroy the machine, you can run `vagrant destroy`.

If instead we want to retrieve the status of all existing machines, independently of our position in the filesystem, we can use:

$ sudo vagrant global-status --prune

In the command above we used the --prune option to be sure the command returns up-to-date data, since data is normally cached. The command returns information about the machine id, name, provider, state, and base directory:

id       name    provider state   directory                           
----------------------------------------------------------------------
6023917  default libvirt running /home/doc/vagrant                   

Suspending, resuming, turning down and destroying a virtual machine

Vagrant provides a set of commands which can be used to manage created machines. To suspend a machine, for example, we use the suspend command, either without any argument, in the same directory where the Vagrant file is, or by passing the machine name or id to it.



For instance, to remove the virtual machine we created in this tutorial, which is named “default”, we would run:

$ sudo vagrant suspend default

To resume it, instead:

$ sudo vagrant resume default

The same syntax can be used to halt a machine:

$ sudo vagrant halt default

Finally, to completely remove any trace of a virtual machine from the host system, we use the destroy command. In this case we would run:

$ sudo vagrant destroy default

Managing boxes

Each Vagrant environment is based on a “box”. Generally speaking, a “box” is to Vagrant what an “Image” is to Docker. In this tutorial we used the generic/fedora37 box, which was automatically downloaded the first time we initialized the environment based on it. To manage boxes on our machine, we can use vagrant box sub-commands.

Listing available boxes

To list the boxes available on our machine we use the list subcommand:

$ sudo vagrant box list

In our case we have only one box available:

generic/fedora37 (libvirt, 4.2.14)

The command returns the box name, together with information about the provider and version.

Adding a box

Vagrant automatically downloads a box when we start an environment based on it and it is not yet available on our machine. We can, however, explicitly add boxes using the add subcommand. To manually add the “generic/fedora37” box, for example, we would run:

$ sudo vagrant box add generic/fedora37 



If a box is compatible with many providers we are prompted to choose which one to use:

==> box: Loading metadata for box 'generic/fedora37'
    box: URL: https://vagrantcloud.com/generic/fedora37
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.

1) hyperv
2) libvirt
3) parallels
4) virtualbox
5) vmware_desktop

Enter your choice: 

To specify the provider directly, and skip the prompt, we can use the --provider option and pass the provider name as argument:

$ sudo vagrant box add generic/fedora37 --provider libvirt

Various versions of a box are often available. By default Vagrant downloads the latest one, however, a specific version can be requested via the --box-version flag:

$ sudo vagrant box add generic/fedora37 --box-version 4.2.12

Removing a box

The subcommand we use to remove a Vagrant box, is remove:

$ sudo vagrant box remove generic/fedora37 



If we try to remove a box while an environment based on it is running, we are prompted to confirm the action, since it can potentially corrupt the environment itself:

Box 'generic/fedora37' (v4.2.14) with provider 'libvirt' appears
to still be in use by at least one Vagrant environment. Removing
the box could corrupt the environment. We recommend destroying
these environments first:

default (ID: 62402eb05ed84825a3f95fd33c40d8ed)

Are you sure you want to remove this box? [y/N] 

To remove a specific version of a box, we can, again, use --box-version, while to remove a box associated with a provider we use the --provider option. Finally to remove all the available versions of a box, we can pass the --all flag.

Conclusions

In this article we talked about Vagrant, a free and open source tool which can be used to easily replicate development environments on different operating systems, using several virtual machines providers, such as Virtualbox, Vmware or libvirt. We saw how to install Vagrant on some of the most used Linux distributions, how to create a Vagrantfile and how to manage virtual environments and boxes. Here we just mentioned basic notions, which should be enough to let you perform your first steps with Vagrant. Please consult the official documentation for a more in-depth knowledge and more specific use-cases.



Comments and Discussions
Linux Forum