Tech Chorus

Introduction To Vagrant

written by Sudheer Satyanarayana on 2019-08-26

Introduction

Vagrant is a glue tool to build and manage virtual environments. You might have been using libvirt, virt-manager or similar tools to build and manage virtual guest operating systems. Vagrant provides a better experience to do the same. I recommend adding Vagrant to the developer and DevOps consultant's toolbox.

To quote HashiCorp: "HashiCorp Vagrant provides the same, easy workflow regardless of your role as a developer, operator, or designer. It leverages a declarative configuration file which describes all your software requirements, packages, operating system configuration, users, and more."

On your host machine, install Vagrant and libvirt. Vagrant works with many providers such as libvirt, and VirtualBox. My favorite is libvirt. The integration between Vagrant and libvirt is provided by Vagrant-libvirt.

Without tools like Vagrant, if you are manually installing virtual guest machines, you have to bear some burden:

  1. No easy way to replicate guests on multiple devices. If you use a laptop and a desktop, porting the environment is error-prone and adds some manual steps.
  2. No easy way to destroy and re-create the guest machines with a desired state.
  3. Manually configure the guest networking and storage.
  4. No version control. It's hard or impossible to go back to a previous version. Branching is probably hard to even imagine.
  5. Sharing the development environment with others is probably reduced to writing a set of instructions in a document.

Installing Vagrant

Fedora 30:

sudo dnf install vagrant vagrant-libvirt

Ubuntu 18.04:

sudo apt install vagrant vagrant-libvirt

First Vagrant Example

Here's a sample Vagrantfile to bring up a CentOS 7 virtual guest. Create the file Vagrantfile and add the following contents:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.hostname = "mycoolguest.example.com"
  config.vm.post_up_message = "Happy development"
end

If you do not know Ruby, don't freak out. You don't need to know Ruby to write beginner to intermediary Vagrantfiles. For most use cases, you can simply adjust the templates from this blog post or other resources.

In the Vagrantfile, we are describing our guest virtual machine. We want to use the image centos/7. These images are hosted on Vagrant Cloud. We assign the hostname mycoolguest.example.com to our guest OS. Once the guest is booted up, Vagrant will show the post_up_message.

To bring up the guest virtual machine:

sudo vagrant up

When you run this command for the first time, Vagrant adds .vagrant directory. If you are using Git, make sure to add the file path to your .gitignore. We don't want .vagrant directory in our version control.

To ssh onto the guest, cd to the directory containing Vagrantfile

cd DIRECTORY_CONTAINING_Vagrantfile
sudo vagrant ssh

That's all it takes to bring up a virtual guest. Simply pull this Vagrantfile from your version control on another device and run vagrant up, you will have your new VM guest in a few seconds or minutes depending on a few factors such as availability of cached image and your hardware performance.

Removing The Need To Use Sudo By Adding A Polkit Policy

By default, the host OS will require you to use sudo for commands like vagrant up. You can modify the behavior by adding a policy in Polkit. Create the file /etc/polkit-1/localauthority/50-local.d/vagrant.pkla and add the following contents:

[Allow youruser libvirt management permissions]
Identity=unix-user:YOUR_USERNAME
Action=org.libvirt.unix.manage
ResultAny=yes
ResultInactive=yes
ResultActive=yes

Replace YOUR_USERNAME with your Linux username. Henceforth, you don't have to use sudo for commands like vagrant up.

Another Vagrant Example: Ubuntu 18.04

Play with the Vagrantfile to understand it better. For example, try changing the image to ubuntu.

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"
end

Sharing Files Between Host And Guest

Vagrant provides a nifty technique to share files between the host and the guest. You can make a directory on the host available to the guest. Vagrant provides a few options to share files. In this post, I will provide an example using vagrant-sshfs. Install the plugin:

If your OS provides the vagrant-sshfs package, you should use it.

On Fedora:

dnf install  vagrant-sshfs

Alternatively, you can install the plugin using vagrant plugin install command:

vagrant plugin install vagrant-sshfs

Add this block of code to your Vagrantfile:

  config.vm.provider "libvirt" do |lvt, override|
   override.vm.synced_folder ".", "/vagrant", type: "sshfs"
  end

We're overriding the default behavior. If the provider is libvirt, we want the current directory on host, represented by . to be mounted on /vagrant on the guest using the sshfs network filesystem. Here's the complete Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"

  config.vm.provider "libvirt" do |lvt, override|
   override.vm.synced_folder ".", "/vagrant", type: "sshfs"
  end

end

Reload the guest:

vagrant reload

SSH onto the guest and list the contents of /vagrant

vagrant ssh
ls /vagrant/

You should also see the mounted filesystem if you execute the command mount on the guest.

Provisioning: Shell

Vagrant has a concept of provisioners. Simply put, a provisioner executes your programs after booting up the guest for the first time. Shell provisioner executes your shell script and Ansible provisioner executes your playbook. There are other provisioners available. Let's make a small excercise of installing Apache.

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

Create the file script.sh in the same directory as Vagrantfile and add the following contents:

#!/bin/bash
apt install apache2 -y

When you run vagrant up for the first time, the provisioners are executed. If you modify the Vagrant file later you have to run the provisioner manually.

Here's the complete example with provisioner to install apache2

#!/bin/bash
apt install apache2 -y

Here's the complete Vagrantfile :

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"

  config.vm.network "private_network", ip: "192.168.12.12"
  config.vm.provision "shell", path: "script.sh"

  config.vm.provider "libvirt" do |lvt, override|
    override.vm.synced_folder ".", "/vagrant", type: "sshfs"
    lvt.qemu_use_session = false
  end

end

Execute the Ansible provisioner manually:

vagrant up --provision

When you logon to the guest, you will see that Apache is installed. It is a good idea to use idempotent scripts in provisioners. Idempotent meaning, the script should not change the state of the machine if it is executed again. In our example, when the provisioner is executed for the first time, the apache2 package will be installed. If the provisioner is executed again, apt will see that the package is already installed and there is no need to install it again and hence no action will be taken. You should be aware of this when you are doing thing such as writing to a file. Be aware that when the provisioner is executed a second time, your script might be writing to the file the second time. In such cases, you probably want to write to file conditionally.

Provisioning: Ansible

Vagrant also has a built-in Ansible provisioner. There are two ways to run the Ansible playbook:

  1. Using the Ansible executable on the host
  2. Installing and using the Ansible executable on the guest.

Let's make a small example to install the package htop

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"

  config.vm.network "private_network", ip: "192.168.12.12"
  config.vm.provision "shell", path: "script.sh"

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

  config.vm.provider "libvirt" do |lvt, override|
    override.vm.synced_folder ".", "/vagrant", type: "sshfs"
    lvt.qemu_use_session = false
  end

Run the provisioner again and see if the package htop is installed.

Networking

Vagrant allows you to assign static IP addresses to the guest. This is accomplished by adding one line of definition in Vagrantfile.

 config.vm.network "private_network", ip: "192.168.12.12"

There are couple caveats here. If you are using, libvirt, set the qemu_use_session to false. The second caveat is, don't assign the IP address x.x.x.1 to the guest. x.x.x.1 is typically reserved for gateways. Our Ubuntu example looks like this after adding networking:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"

  config.vm.network "private_network", ip: "192.168.12.12"

  config.vm.provider "libvirt" do |lvt, override|
    override.vm.synced_folder ".", "/vagrant", type: "sshfs"
    lvt.qemu_use_session = false
  end

end

Inserting Your SSH Public Key To The Guest

You can ssh on to the guest machine by executing vagrant ssh. But what if you want to ssh on to the guest directly using the ssh client? Using the shell provisioner you can insert your SSH public key. Here's an example:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu1804"
  config.vm.hostname = "mycoolguestubuntu.example.com"
  config.vm.post_up_message = "Happy development"

  config.vm.network "private_network", ip: "192.168.12.12"
  config.vm.provision "shell", path: "script.sh"

  config.ssh.insert_key=false
  config.ssh.private_key_path = ['~/.vagrant.d/insecure_private_key', '~/.ssh/id_rsa']
  config.vm.provision "file", source: "~/.ssh/id_rsa.pub", destination: "~/.ssh/authorized_keys" 

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

  config.vm.provider "libvirt" do |lvt, override|
    override.vm.synced_folder ".", "/vagrant", type: "sshfs"
    lvt.qemu_use_session = false
  end

end

We are using three configuration lines to accomplish our goal. Firstly, we instruct Vagrant to not insert the keypair. This will result in Vagrant utilizing the default insecure key. Secondly, we are passing a list of SSH private key file paths to be used to ssh onto the guest machine. Vagrant uses the default insecure key to bootstrap the machine. Thirdly, we instruct Vagrant to copy the file ~/.ssh/id_rsa.pub on the host to the guest path ~/.ssh/authorized_keys. At this point, Vagrant guest uses allows only our secure private key for SSH authentication.

Now you can use ssh using your private key like:

ssh vagrant@192.168.12.12

The default way of sshing on to the guest also works:

vagrant ssh

Hopefully, you will use this knowledge to build and maintain development and testing environments and share them with others.

Tags: vagrant virtualization libvirt libvirtd linux centos ubuntu portable development environment development environment networking network ssh