In this tutorial, we’ll see how we can manage our development environments with Vagrant. We’ll discuss some use cases, look at the basics of Vagrant and then try out the different configuration options.
2. Use Cases
Vagrant helps us create and configure virtual machine environments. We can describe our environment once and use that configuration whenever we'd like to recreate it. Thus, it can speed up the onboarding process for a project and eliminates the risk of misconfiguring something.
We can even mirror production environments locally. Consequently, our tests can be more accurate, and we can reduce the number of unexpected issues when we deploy our app. This also helps to reproduce problems that occur in the production environment.
Vagrant uses a special configuration file to describe an environment called a Vagrantfile. The reason behind the naming is simple, the name of these configuration files should be Vagrantfile.
We can have one Vagrantfile per project. However, when we run Vagrant commands, it looks for other Vagrantfiles on our filesystem and merges the relevant ones to build the final configuration. For example, we can have a Vagrantfile in our Vagrant home directory with some default configuration and override the project-specific values in the project directory.
Vagrantfiles use the Ruby programming language, but we don’t necessarily need to know Ruby to create, understand or modify these files. Most of the time, we use simple operations, for example, variable assignment. On the other hand, if we know Ruby well, we can make use of it and create more complex Vagrantfiles easily.
Let’s create our Vagrant project in a directory called vagrant-start. We can create an initial Vagrantfile in this directory using the init command:
$ vagrant init hashicorp/bionic64
It produces the following output, and we can find the generated Vagrantfile in the directory:
A `Vagrantfile` has been placed in this directory. You are now ready to `vagrant up` your first virtual environment! Please read the comments in the Vagrantfile as well as documentation on `vagrantup.com` for more information on using Vagrant.
Let’s take a look at the generated file and understand what each section does. We've removed some comments from the file for clarity:
Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" end
First of all, we can see the version of the configuration: Vagrant.configure(“2”). This means that Vagrant will use version 2 of the configuration object. This is important because of the backward compatibility between different versions of Vagrant.
After this, we can see a configuration for the virtual machine: config.vm.box = “hashicorp/bionic64”. This is a simple variable assignment – we set the name of the box. The value is exactly what we provided in the init command. We’ll talk about boxes in more detail in the next section.
Let’s validate our Vagrantfile with the validate command:
$ vagrant validate
The output should be the following:
Vagrantfile validated successfully.
This command can be useful any time we make changes to the file, and we'd like to make sure it's still valid.
In Vagrant, the base image for an environment is called a box. These are versioned images, and they help us quickly clone a virtual machine.
In the previous section, we created our Vagrantfile and set our box name to “hashicorp/bionic64“. This refers to an Ubuntu 18.04 LTS environment. In other words, we can start a virtual machine with Ubuntu by simply using this box.
We can find other public, predefined boxes in the Vagrant Cloud. They can serve as a good starting point to set up our environment. However, we must be careful because there are only two types of official boxes. These are the HashiCorp and Bento boxes.
We can even create our own base box and upload it to Vagrant Cloud.
4.1. Managing Boxes
Let’s start a virtual machine using the previously defined Vagrantfile. The up command in Vagrant creates and configures our environment:
$ vagrant up
This command downloads the specified box and starts the virtual machine:
Bringing machine 'default' up with 'virtualbox' provider... ==> default: Box 'hashicorp/bionic64' could not be found. Attempting to find and install... default: Box Provider: virtualbox default: Box Version: >= 0 ==> default: Loading metadata for box 'hashicorp/bionic64' default: URL: https://vagrantcloud.com/hashicorp/bionic64 ==> default: Adding box 'hashicorp/bionic64' (v1.0.282) for provider: virtualbox default: Downloading: https://vagrantcloud.com/hashicorp/boxes/bionic64/versions/1.0.282/providers/virtualbox.box ==> default: Successfully added box 'hashicorp/bionic64' (v1.0.282) for 'virtualbox'!
As we can see, Vagrant didn’t find the box on our machine, so it downloaded it from Vagrant Cloud. To do this, it had to look into the metadata of the box. Boxes can have an optional metadata file that contains information about versioning, providers, or simply the name and description of the box.
We can manage boxes directly with the vagrant box command. Let’s list the downloaded boxes:
$ vagrant box list
hashicorp/bionic64 (virtualbox, 1.0.282)
This means that Vagrant won’t need to download this box again when we'd like to use it later.
Let’s stop the virtual machine using the vagrant destroy command:
$ vagrant destroy
This command stops the virtual machine and deletes all related resources. However, the downloaded boxes remain available locally. To remove them, we'd need to run the vagrant box remove command.
Boxes are the package format in Vagrant, but we need providers to be able to run them. Furthermore, boxes are provider-specific. This means that a Vagrant box defined for only Hyper-V is incompatible with VirtualBox. However, a box can support multiple providers to overcome this limitation.
In our previous examples, we used VirtualBox because it's the default provider. Vagrant supports three providers by default: VirtualBox, Hyper-V, and Docker. In other words, by default, we can run VirtualBox or Hyper-V virtual machines and even Docker containers in our Vagrant environment.
Let’s list the available boxes and examine the output of this command:
$ vagrant box list
The output consists of two important parts:
hashicorp/bionic64 (virtualbox, 1.0.282)
First of all, we can see the box name that we used in the previous example. Besides this, there is the provider and the version of the box in parentheses. This is useful information here, so we know which provider is used by a specific box.
Additional providers can be installed using the plugin system in Vagrant. It should be a fairly simple process because the CLI provides commands to manage plugins. Moreover, we can develop new, custom providers to further extend our possibilities.
Usually, simple boxes aren't enough for our use cases. They provide only the essentials for us. We need to install and configure the necessary components to have a useful development environment. We can do this every time by hand, but it would be time-consuming and error-prone. Provisioning automates the process of customizing the development environment. It happens when we first create the environment with the vagrant up command, but we can run it manually, too.
We can have multiple provisioners per project. On top of that, there are different types of provisioners as well. For example, we can run shell scripts or copy files to the virtual machine. Let’s define a simple shell provisioner in our Vagrantfile:
Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" config.vm.provision "shell", inline: "echo Hello from provisioner" end
This prints the “Hello from provisioner” message when the provisioning runs:
$ vagrant up
==> default: Running provisioner: shell... default: Running: inline script default: Hello from provisioner
We successfully created and initialized our development environment in the previous steps, but we haven’t been able to access it yet. In order to access the machine, we need to configure networking. We can connect to private or public networks or configure port forwarding.
The most basic configuration option is port forwarding. In this case, we access a port of the guest machine on a port of our host.
Let’s create a new development environment and configure this. Firstly, for our example, we need to create a new provisioning script named “provision.sh” that installs Nginx when we create the environment:
#!/usr/bin/env bash apt-get -y update apt-get install -y nginx service nginx start
After this, let’s create a Vagrantfile that uses this script for provisioning:
Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" config.vm.provision :shell, path: "provision.sh" config.vm.network "forwarded_port", guest: 80, host: 8080 end
We used the “config.vm.network” setting in our Vagrantfile to forward port 8080 of our host to port 80 in the guest. Let’s run this example and open http://localhost:8080/ in our browser. We can see an Nginx welcome screen, meaning that we can reach the web server inside the virtual machine.
In this article, we looked at Vagrant, a powerful tool that helps us to adopt the Infrastructure-as-Code practice in our projects. We initialized a development environment using virtual machines.
We used Vagrantfiles to specify which box we'd like to run and used provisioning to automate the initial setup. Lastly, we configured port forwarding to access a web server inside the virtual machine.