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.
- Provisioning: Instead of manually specifying the configuration of a machine, the state is declared in a
Vagrantfile
. These files allow the user to define configuration such as IP addresses, shared folders and even accept initialization scripts to install necessary packages when the VM is created. - Replication: Once a
Vagrantfile
is defined, this file can be shared with other users/developers who can then simply create a replica of the machine by simply running a command.
How Does It Work?
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
- Basic knowledge of shell/command line.
- An machine with a compatible OS and shell access with enough resources to run VMs.
- A VM provider. My personal recommendation is VirtualBox.
- A Vagrant installation.
Necessary Terminology
Term | Description |
---|---|
Virtual Machine (VM) / Guest | A software emulation of a real machine. |
Host (machine/OS) | The environment which hosts the Virtual Machine (ex: a laptop or even a hypervisor). |
VM Provider | A program such as VMWare, VirtualBox etc., that creates/manages VMs. |
Vagrant Box | A 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
"2"
stands for the version of configuration object. At the time of writing this post, values1
and2
are supported.config.vm.box
is the Vagrant Box corresponding to the OS (+additional packages) that will run on this VM. This box will be downloaded on the first run of this VM. The full list of supported VMs is on the Vagrant Box Catalog.config.vm.provider
configures the VM provider (virtualbox
in this case).v.memory
: the amount of RAM in bytes to allocate to this VM.v.cpus
: the number of virtual CPUs to allocate to this VM.v.name
: an optional name for this VM.
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
- Official Vagrantfile reference.