Vagrant Provisioning

Using Shell Provisioner to Configure Virtual Systems

Joaquín Menchaca (智裕)
6 min readAug 9, 2018

--

One of the coolest and core features of Vagrant is the ability to provision systems, or in other words configure systems to an initial state. Vagrant can do this with through shell scripts, popular change configuration systems, or through containers with Docker.

This is a small overview of this system using the shell provisioner. We’ll run hello_web.sh installs a web server with some files on an Ubuntu 16.04 system.

Prerequisites

This small tutorial requires that Vagrant is installed along with virtual system VirtualBox. These instructions use Curl and Bash, so if you do not have those, you’ll use whatever is equivalent in your shell environment. For example, Windows users would need to find the equivalent PowerShell commands, and utilize something like the System.Net.WebClient object in place of Curl.

I created some previous guides on installing Vagrant and other tools, that may be useful depending on your operating system:

Installing Vagrant on macOS (using Homebrew)

A small guide that runs through using Homebrew to install Vagrant and VirtualBox.

Installing Vagrant on Windows (using Chocolatey)

A small guide that runs through using Chocolatey to install Vagrant and VirtualBox.

Installing Vagrant on Fedora

A small guide that runs through installing Vagrant and VirtualBox on Fedora.

Part I: Provisioning Ubuntu Systems

Ubuntu 16.04 Xenial Xerus

For this part, we’ll create provisioning script for Ubuntu 16.04 Xenial Xerus.

Vagrant Structure

To get started we need to create a working directory and support files:

WORKAREA=${WORKAREA:-"${HOME}/vagrant-shell"}mkdir -p ${WORKAREA}/scripts
touch ${WORKAREA}/{Vagrantfile,scripts/hello_web.sh}
cd ${WORKAREA}

This will create a base line structure that we’ll use to create a virtual development environment, in which to develop provisioning scripts.

~/vagrant-shell
├── Vagrantfile
└── scripts
└── hello_web.sh

Vagrant Configuration

Now we need to fill out the Vagrantfile configuration script to get started. Here’s a configuration for Ubuntu 16.04 Xenial Xerus:

script_path = './scripts'Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.network "forwarded_port", guest: 80, host: 8080
####### Provision #######
config.vm.provision "shell" do |script|
script.path = "#{script_path}/hello_web.sh"
script.args = %w(apache2 apache2 /var/www/html)
end
end

With this in place, we can bring up our Vagrant system without provisioning:

vagrant up --no-provision

Provisioning Script

Our provisioning script will install Apache HTTP server and create some content. Edit the scripts/hello_web.sh with this:

#!/usr/bin/env bash#### Set variables with intelligent defaults
APACHE_PACKAGE=${1:-'apache2'}
APACHE_SERVICE=${2:-'apache2'}
APACHE_DOCROOT=${3:-'/var/www/html'}
#### Download and Install Package
apt-get update
apt-get install -y ${APACHE_PACKAGE}
#### Start, Enable Service
systemctl start ${APACHE_SERVICE}.service
systemctl enable ${APACHE_SERVICE}.service
#### Create Content
cat <<-'HTML' > ${APACHE_DOCROOT}/index.html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
HTML

We can test this provisioning script with:

vagrant provision

Testing the Solution

We can test the solution out using by running:

curl -i http://127.0.0.1:8080

This will return something like:

HTTP/1.1 200 OK
Date: Wed, 08 Aug 2018 08:41:30 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Wed, 08 Aug 2018 08:39:09 GMT
ETag: "37-572e871b6e929"
Accept-Ranges: bytes
Content-Length: 55
Content-Type: text/html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>

Part II: Adapting to Other Operating Systems

Debian • Ubuntu • CentOS • Fedora • Gentoo • ArchLinux

We can expand our shell provisioning system to support other operating system distributions (or distros for short).

We must first update our Vagrantfile to target the desired Vagrant image for the distro we want, and also pass in the correct arguments to our shell script for the ① package name, ② service name, and ③ docroot, that correspond to the distro we are targeting.

Further, we have to update our hello_web.sh provisioning script to support each of these distros.

Update Vagrantfile

As we added the ability to pass arguments to the shell script from Vagrant, we can easily modify our Vagrantfile to support other operating systems.

If you are using the same environment, first remove the current environment with vagrant destroy, then we can modify the existing Vagrantfile to support CentOS 7 for example:

script_path = './scripts'Vagrant.configure("2") do |config|
config.vm.box = "centos/7"
config.vm.network "forwarded_port", guest: 80, host: 8080
####### Provision #######
config.vm.provision "shell" do |script|
script.path = "#{script_path}/hello_web.sh"
script.args = %w(
httpd
httpd
/var/www/html
)
end
end

In this Vagrantfile configuration, we simply swapped the config.vm.box to point to the desired distro, and pass the appropriate script.args for the package name, service name, and docroot.

These values can vary depending on operating system distro you are using (and also version of the distro). Here’s a chart of the differences from popular distros:

Vagrant box name and arguments for package, service, docroot

As another example, using Gentoo, the Vagrantfile would be changed to this:

script_path = './scripts'Vagrant.configure("2") do |config|
config.vm.box = "generic/gentoo"
config.vm.network "forwarded_port", guest: 80, host: 8080
####### Provision #######
config.vm.provision "shell" do |script|
script.path = "#{script_path}/hello_web.sh"
script.args = %w(
www-servers/apache
apache2
/var/www/localhost/htdocs
)
end
end

Updating Provisioning Script

We also need to evolve our provisioning script scripts/hello_web.sh to support different distros. This can get quite complex, as different distros have different package management and service management systems.

#!/usr/bin/env bash#### Set variables with intelligent defaults
APACHE_PACKAGE=${1:-'apache2'}
APACHE_SERVICE=${2:-'apache2'}
APACHE_DOCROOT=${3:-'/var/www/html'}
#### Download and Install Package
DISTRO=$(
awk -F= '/^ID=/{print tolower($2) }' /etc/os-release \
| tr -d '"'
)
case "${DISTRO}" in
centos|rhel)
yum install -y ${APACHE_PACKAGE}
;;
fedora)
dnf install -y ${APACHE_PACKAGE}
;;
debian|ubuntu)
apt-get update
apt-get install -y ${APACHE_PACKAGE}
;;
gentoo)
emerge ${APACHE_PACKAGE}
;;
arch)
pacman -Syu --noconfirm
pacman -S --noconfirm ${APACHE_PACKAGE}
;;
*)
echo "Distro '${DISTRO}' not supported" 2>&1
exit 1
;;
esac
#### Start, Enable Service
case "${DISTRO}" in
centos|rhel|fedora|debian|ubuntu|arch)
systemctl start ${APACHE_SERVICE}.service
systemctl enable ${APACHE_SERVICE}.service
;;
gentoo)
rc-update add ${APACHE_SERVICE} default
/etc/init.d/${APACHE_SERVICE} start
;;
*)
echo "Distro '${DISTRO}' not supported" 2>&1
exit 1
;;
esac
#### Create Content
cat <<-'HTML' > ${APACHE_DOCROOT}/index.html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
HTML

Shell Provisioning is Complex — Don’t do it

As you can see, wiring in all these variations can get quite complex to script in shell or other scripting language, as you have to handle all these variations.

As an alternative, a change configuration solutions like CFEngine or CAPS (Chef, Ansible, Puppet, Salt Stack) have the ability to abstract the specific operating system resources, conceptually, you just do something conceptually like this:

package($package_name).install()
service($service_name).enable().start()
file("$docroot/index.html", $source).create()

With a good change configuration, you don’t have to worry about the unique package system (yum, dnf, apt-get, emerge, or pacman) and instead worry about a generic package resource.

Similarly, with change configuration, you don’t have to worry about the underlying service management system (System V init system with /etc/init.d scripts, Upstart with Ubuntu 14 and CentOS 6, or newer SystemD init system), and instead just worry about a generic service resource.

Wrapping Up

That is essentially all there is to it, the take away is that you can use Vagrant to configure your system with a variety of solutions. This can be used to rapidly develop and test on virtual systems at no cost other than the time it takes to initially download the image from VagrantCloud.

Shell scripting is great for quickly learning how to configure a new operating system or learning a new service, but can get quite complex, and for this reason, long term, consider moving toward proper change configuration solution that will have abstractions for system resources.

--

--

Joaquín Menchaca (智裕)

DevOps/SRE/PlatformEng — k8s, o11y, vault, terraform, ansible