Create and share virtual environments with Vagrant
Introduction
When prototyping or developing stuffs, it is often a good idea to use Virtual Machines (VMs) in order to isolate everything and avoid messing up your computer. You are then able to test something and recreate the machines when everything is broken.
However, creating VMs can be painful and repeating this process several times can be very long. Using tools and concepts close to Infrastructure as Code (IaC) to automatically deploy the machines is the right idea and Vagrant is here to help you.
What is Vagrant basically ?
Let’s take an example with a Linux Operating System (OS) and KVM/libvirt as hypervisor. Let say that you simply want to test the new major version of your preferred OS and be sure that every tools you are using are still working. You probably won’t take the risk to completely erase your computer and install directly the OS. The best option could be to use a VM. You will then deploy the machine (via CLI with virsh
/virt-install
commands or via GUI with Virtual Machine Manager or Cockpit), eventuelly follow the installation process, reboot, install your tools and reconfigure your environment before (finally !) being able to test if everything is working as expected… Very long workflow that you have to reiterate for each major version release !
Another solution could be to describe once for all what you want to do and take this recipe each time you need it. This is basically what Vagrant is made for : you describe almost evrything in one file (called Vagrantfile) and you are done ! It is finally a simple abstraction layer above your hypervisor which takes over VM management and configuration.
We are talking about KVM hypervisor in our example but we could have choosen whichever one. Vagrant has a concept of providers which is basically a plugin that Vagrant can use to communicate with an hypervisor. VirtualBox is supported out of the box but you will have to install a custom provider to let Vagrant manage a KVM hypervisor via libvirt.
First step : install Vagrant
Depending on your OS, there are multiple ways to install Vagrant. You can follow the official documentation or an OS specific documentation. We will show how to do this with Fedora.
If libvirt is not already installed, there is a package group for that (called Virtualization). Vagrant and the libvirt provider is also shipped in another package group (called Vagrant). As a rusult, you can install both with :
sudo dnf group install virtualization vagrant
You should then enable livirtd
to automatically start and add your current user to libvirt
group in order to avoid password prompting each time you start a VM. These can be done with the following commands :
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt ${USER}
newgrp libvirt
Quick start : your first Vagrantfile
As already explained, the power of Vagrant is to simply describe in a single file the whole infrastructure you want to create. The syntax uses Ruby language but don’t worry if you know nothing about this language, you basically just have to declare variables. Let’s have a look with these lines thant you can copy and past in a file named Vagrantfile
:
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
end
This Vagrantfile simply declares a VM which uses a Fedora 41 Cloud Base image. Juste note that OS images used by Vagrant are called boxes and are basically packages embedding metadata, configuration files and a VM image specifically configured for Vagrant. Hashicorp (the company behind Vagrant) hosts a repository with many boxes publicly available. As each box has a specific version for each provider (KVM/libvirt, VirtualBox, …), be sure that the box you want is compatbile with the provider(s) you want to use.
To launch the VM, the only thing you have to do is to run the following command in the directory where the Vagrantfile is :
vagrant up
Note : For those already running Fedora, the Vagrant version shipped with the distribution uses libvirt daemon session mode. As a result, Vagrant is running in an unprivileged mode and only has the same rights than the current user which is more safer. However, some functionalities (as network operations) require root permissions and Vagrant needs to run in system mode. This can be done by adding a libvirt option as described below :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
config.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
end
Provisioning the machine
Launching VM is interesting but in our example above we talked about making some tests in our new machine. Automatically do stuffs is what provisioners are made for. Once again, you can observe that Vagrant is very modular as there are many different provisioners available as detailed in Vagrant documentation.
We are going to use Ansible provisioner to do stuffs in our VM. We won’t go too much into details on how Ansible works. The only important thing to know about Ansible is that it allows you to discribe in a file (called a playbook) a sequence of tasks that will be run inside the VM.
We first have to install Ansible on the host alongside Vagrant. On Fedora, this can be done with the following command :
sudo dnf install ansible
Just copy and past in a file called playbook.yml
the lines below. This file must be in the same directory as the Vagrantfile. This Ansible playbook simply updates the machine.
---
- hosts: all
gather_facts: true
tasks:
- name: Upgrade all packages
ansible.builtin.dnf:
name: "*"
state: latest
register: update
become: true
- name: Reboot to apply updates
ansible.builtin.reboot:
post_reboot_delay: 60
reboot_command: reboot
when: update.changed
become: true
You can then update you Vagrantfile to call the playbook once the VM is started for the first time :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
config.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
config.vm.provision "ansible" do |ansible|
ansible.playbook = "playbook.yml"
end
end
If it is not the first time you launch the VM, you can force provisioning with the following command :
vagrant up --provision
Multi-machine infrastructure
The examples above were only launching a single machine. Obviously, you can declare multiple machines that will be launched at the same time. The syntax is slightly different but the idea is the same : you just have to include each machine configuration variables inside a config.vm.define
method.
Vagrant.configure("2") do |config|
config.vm.define "vm1" do |hostconfig|
hostconfig.vm.box = "fedora/41-cloud-base"
hostconfig.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
end
config.vm.define "vm2" do |hostconfig|
hostconfig.vm.box = "fedora/41-cloud-base"
hostconfig.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
end
end
You can even factor configuration variables that are the same accross machines. This way, the Vagrantfile just above would become :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
config.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
config.vm.define "vm1" do |hostconfig|
end
config.vm.define "vm2" do |hostconfig|
end
end
Naturaly, it is still possible to provision the VMs. In case we want to use the same playbook to provision each VM, the first approach would be to declare the playbook at config
level :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
config.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
# Provisionning
config.vm.provision "ansible" do |ansible|
ansible.playbook = "playbook.yml"
end
config.vm.define "vm1" do |hostconfig|
end
config.vm.define "vm2" do |hostconfig|
end
end
This approach is not wrong but, as explained in the Vagrant documentation, Vagrant will provision VM in sequence so that two different playbooks will be launch. As the power of Ansible is to provision multiple VMs using parallelism (this means lauching a single playbook will provision as many VMs as you want), if you want to keep this behavior, you will have to modify a little bit the syntax. The Vagrantfile just above would thus become :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
config.vm.provider :libvirt do |libvirt|
libvirt.qemu_use_session = false
end
N = 2
(1..N).each do |machine_id|
config.vm.define "vm#{machine_id}" do |hostconfig|
# Only execute once the Ansible provisioner,
# when all the machines are up and ready.
if machine_id == N
hostconfig.vm.provision :ansible do |ansible|
# Disable default limit to connect to all the machines
ansible.limit = "all"
ansible.playbook = "playbook.yml"
end
end
end
end
end
Multi-provider compatibility
Another good thing with Vagrant is that you can share your Vagrantfile with others. If you are developing stuffs and using Vagrant to make tests in one or multiple VMs, you can easily share your test infrastructure with other developers.
In case these others are using Vagrant with VirtualBox provider whereas you are using it with libvirt provider, it is still possible to make your Vagrantfile compatible with both. As described in the documentation, Vagrant has a default provider that will be used to launch VMs. It is generally VirtualBox but it can be another one depending on the distribution (as Fedora is not providing a VirtualBox package, libvirt is the default provider).
As a result, the minimum Vagrantfile below should work exactly the same on different environments.
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
end
As a box is specific to a provider and an architecture, Vagrant will automatically get the box compatible with your own environment (if such a box exists of course).
However, it is sometimes necessary to explicitly write provider specific parameters. Some values are indeed managed by the provider and need to be re-written for all providers that will be used with the Vagrantfile. Let say that we discribe a VM with an amount of 8 Gb of memory and 4 CPUs. If this Vangrantfile will be used with VirtualBox and libvirt providers, it should then looks like :
Vagrant.configure("2") do |config|
config.vm.box = "fedora/41-cloud-base"
# VirtualBox specific parameters
config.vm.provider :virtualbox do |vbox|
vbox.memory = 8192
vbox.cpus = 4
end
# libvirt specific parameters
config.vm.provider :libvirt do |libvirt|
libvirt.memory = 8192
libvirt.cpus = 4
end
end
Conclusion
Vagrant is the perfect tool to easily describe, configure and eventually share a VM infrastructure. As shown in this post, you can quickly start to write a Vagrantfile. The Ruby syntax let you create more complicated stuffs. Don’t hesitate to also read Vagrant or associated plugins documentation to see the power of this tool.
Unfortunately, since v3.4.0, Vagrant does not have an open source license anymore as the new Business Source License restricts the use of the software to certain users. Such a change has impacts on how the product is integrated in open-source distribution like Fedora which only supports open-source softwares in their official repositories. The package is then distributed in version before v3.4.0 which does not allow user to easily get an updated version.