Vagrant - Provisioning 1/2

Vagrant is a powerful tool, that can help to create and manage VMs on different operating systems. Very often you will need to add more software to these VMs or test your new web project. This can be achieved with provisioners. In this article, I will tackle file and shell provisioners.

Vagrant - Provisioning 1/2

Vagrant is a powerful tool, that can help to create and manage VMs on different operating systems. Very often you will need to add more software to these VMs or test your new web project. This can be achieved with provisioners. In this article, I will tackle file and shell provisioners.

If you haven't heard of Vagrant, please have a look at the "Vagrant - Getting Started" article.

Provisioning

Vagrant can do provisioning on each creation (vagrant up) automatically or manually (vagrant provision). You can copy files, apply scripts, create containers or run Ansible playbooks. This can be very useful to prepare a complete environment with different tools installed and configurations applied.

In addition, you can use these provisioners also to test the functionality of these tools. Since everything is on your local machine, there is no need to push something to a test or production system, but test your scripts, playbooks and chef containers locally.

Vagrant provisioners can be tuned and tweaked to your liking with lots of options. Please have a look at the documentation for all the details.

Let's see how provisioners work with some simple examples.

Hint
The guide is tested on Fedora 34 with Vagrant 2.2.16.

File

The first and most simple provisioner is a simple file upload. It works by simply copying a file via SCP/SSH as the SSH user to the Vagrant machine. You can determine the user by running the command vagrant ssh-config. Be aware, that you can only copy files to a location, where this user has permissions to create files.

Let's see this in an example. First, you have to create a Vagrantfile as seen below.

Vagrant.configure("2") do |config|

  # machine
  config.vm.define "fedora" do |fedora|
    fedora.vm.box = "fedora/34-cloud-base"
    fedora.vm.hostname = "fedora"
  end

  # provision
  config.vm.provision "file" do |file|
    file.source = "file.conf"
    file.destination = "~/file.conf"
  end
end
Vagrantfile

You can see, there is a new # provision section introduced. In this case, we want to copy a file "file.conf" to the home directory of the SSH user.

You can start the Vagrant instance now, which will also trigger the provision step during the creation. If you changed something, you can also trigger the provision step manually.

# Start the instance
$ vagrant up

# Provision manually
$ vagrant provision
==> fedora: Running provisioner: file...
    fedora: file.conf => ~/file.conf

If you connect to the machine, you can verify, that the file is in its place.

# Connect to the vagrant instance
$ vagrant ssh

# Check for the files
$ ls -la
...
-rw-rw-r--. 1 vagrant vagrant   51 May 25 08:11 file.conf
...

You can use this feature to copy scripts, configuration files and binaries to your instance, so it can be used there. You can also check out the above example in this repository.

Shell

In case you need something more, like running a script during the creation, you can check out the Shell provisioner. This allows to execute single commands and shell scripts during the creation and afterwards. This is especially useful, if you don't have any experience with Chef, Puppet or Ansible.

You can differentiate in two general approaches: "Inline shell commands" and "prepared scripts". Each of them has its benefits.

Inline shell commands

Inline shell commands can be very useful, if you just want to trigger a single command. This is very useful, if you want to update your instance on creation or trigger something inside the machine.

Be aware, that the shell provisioner is always used with privileges (sudo for Linux based OS). This behavior can be tuned with the privileges (boolean) option.

Let's see an example. We just need to create a Vagrantfile with the below content.

Vagrant.configure("2") do |config|

  # machine
  config.vm.define "fedora" do |fedora|
    fedora.vm.box = "fedora/34-cloud-base"
    fedora.vm.hostname = "fedora"
  end

  # provision
  config.vm.provision "shell" do |shell|
    shell.inline = "dnf update -y"
  end
end
Vagrantfile

Starting the machine will also trigger the shell command and therefore update the system.

# Start machine
$ vagrant up
...
==> fedora: Running provisioner: shell...
    fedora: Running: inline script
...
    fedora: ================================================================================
    fedora: Install   9 Packages
    fedora: Upgrade  92 Packages
    fedora: Total download size: 282 M
...

We will end up with an updated instance, that is updated. If you may know, it is a good idea to reboot a machine after an update and the shell provisioner has some options to take care of this. We just need to update the Vagrantfile.

Vagrant.configure("2") do |config|

  # machine
  config.vm.define "fedora" do |fedora|
    fedora.vm.box = "fedora/34-cloud-base"
    fedora.vm.hostname = "fedora"
  end

  # provision
  config.vm.provision "shell" do |shell|
    shell.inline = "dnf update -y"
    shell.reboot = "true"
  end
end
Vagrantfile

Vagrant will take care of the shell command and the reboot, and you will end up with an updated and rebooted deployment.

Prepared scripts

If you want to prepare more complex setups, you should have a look at prepared scripts. You just need to create your script file and add it to your Vagrantfile for provisioning. Below, I will show how you can write a small script to install and enable the Apache httpd server in a Fedora instance.

First, you need to create a Vagrantfile like this.

Vagrant.configure("2") do |config|

  # machine
  config.vm.define "fedora" do |fedora|
    fedora.vm.box = "fedora/34-cloud-base"
    fedora.vm.hostname = "fedora"
  end

  # provision
  config.vm.provision "shell" do |shell|
    shell.path = "script.sh"
  end
end
Vagrantfile

Let's also create the script with the below content.

#!/usr/bin/env bash

echo "Install httpd Packages"

dnf install -y httpd
dnf clean all

echo "Start httpd Service"
systemctl enable --now httpd

echo "$(hostname -I)"

exit 0
script.sh

Starting the instance will apply the script and therefore install httpd and start the service. In addition, it will output the IP addresses assigned to it with the command echo "$(hostname -I)" at the end.

# Start the instance
$ vagrant up
...
==> fedora: Running provisioner: shell...
    fedora: Running: /tmp/vagrant-shell20210525-15681-ujjacd.sh
...
    fedora: 192.168.122.114
...

You can curl this IP address to if it worked.

# Check if it works
$ curl 192.168.122.114
<!doctype html>
<html>
  <head>
  ...
  </body>
</html>

Code

I have uploaded a bunch of examples, which you can use to build your own provisioners. These also include some examples from the "Vagrant - Getting Started" article, and the repository is under active maintenance.

vagrant-templates
A collection of vagrant templates for different purposes.

Vagrant provides additional documentation for the provisioners. Please have a look at these, too.

File Uploads - Provisioning | Vagrant by HashiCorp
The Vagrant file provisioner allows you to upload a file or directory from thehost machine to the guest machine.
Shell Scripts - Provisioning | Vagrant by HashiCorp
The Vagrant Shell provisioner allows you to upload and execute a script withinthe guest machine.

Conclusion

With these simple provisioners, you are now able to enhance your Vagrantfiles with simple steps, some files or even shell scripts. This makes it very easy to prepare machines with additional tools and configurations.

In the next article, I will have a look at Deployments with Ansible, which is very useful for Ansible development, but also for sharing my development environment.