Skip to content

Using Vagrant to test Ansible roles

Continuous integration will tell you whether a role still applies cleanly, but while you are writing one you want a faster loop: a machine you can throw away, apply the role to, break, and recreate. A local hypervisor such as VirtualBox plus Vagrant gives you exactly that for a role like ansible-role-ntp.

This post was written in 2014 for Vagrant 1.x and Ansible 1.x. The Vagrantfile below has been updated so it still works: the original precise32 box and the files.vagrantup.com box URLs no longer exist, and ansible.sudo was replaced by ansible.become in Vagrant 1.7. If you are setting up role testing from scratch today, look at Molecule first — it has become the standard tool for this and runs the same idea against containers or cloud instances. The Vagrant approach remains a good fit when the role touches kernel modules, services or anything else that is awkward in a container.

Vagrant

Vagrant is a wrapper around your virtualisation software. It takes care of downloading the base image, booting it, provisioning it, connecting to it and destroying it again — over and over. What it should do is described in a single file per project, the Vagrantfile.

Start by installing Vagrant.

A Vagrantfile for a Role

vagrant init writes a heavily commented default Vagrantfile, which is worth reading once. Below is the trimmed-down version for the role ansible-role-ntp:

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

  config.vm.box = "debian/bookworm64"

  config.vm.define "localhost" do |l|
    l.vm.hostname = "localhost"
  end

  config.vm.provider :virtualbox do |vb|
    vb.name = "ansible-role-ntp"
  end

  config.vm.provision "ansible" do |ansible|
    ansible.become = true
    ansible.playbook = "role.yml"
    ansible.verbose = "v"
    ansible.host_key_checking = false
  end
end

Let me go through it.

Box

config.vm.box = "debian/bookworm64"

The base image. Vagrant downloads it from the public box registry on first use and caches it, so this only costs time once. Pick the distribution your role is supposed to support — and if it claims to support several, add a VM per distribution and test them all.

Network

The default NAT interface already gives the VM outbound connectivity, which is all a provisioning run needs. You only have to declare a network when you want to reach the VM from your host, for instance to run a second playbook against it or to check that the service is actually listening:

config.vm.network "private_network", ip: "192.168.56.10"
config.vm.network "forwarded_port", guest: 8080, host: 8080

Define

The role playbook role.yml targets localhost, so the VM has to be named localhost as well for Vagrant’s Ansible provisioner to match it:

config.vm.define "localhost" do |l|
  l.vm.hostname = "localhost"
end

Provider

Purely cosmetic, but it makes the VM easy to find in the VirtualBox GUI when you have a dozen roles on your machine:

config.vm.provider :virtualbox do |vb|
  vb.name = "ansible-role-ntp"
end

Provisioner

And the interesting part — Vagrant runs Ansible for us:

config.vm.provision "ansible" do |ansible|
  ansible.become = true
  ansible.playbook = "role.yml"
  ansible.verbose = "v"
  ansible.host_key_checking = false
end

Vagrant generates the inventory itself, pointing at the VM it just booted, and invokes ansible-playbook on your host — so the Ansible you are testing with is your own installation, not one inside the VM.

If the box happens to be missing something Ansible needs, a shell provisioner declared before the Ansible one can bootstrap it. In 2014 that meant installing Python, which the boxes of the day did not ship:

$script = <<SCRIPT
apt-get update
apt-get -qq install python python-pycurl python-apt
SCRIPT

config.vm.provision "shell", inline: $script

Current boxes come with Python preinstalled, so this block is no longer needed — but it is a useful pattern to know for any other prerequisite.

Running It

Boot the VM and apply the role:

vagrant up

Apply the role again — this is the run that matters, because the second run has to report no changes at all if the role is properly idempotent:

vagrant provision

Log in and verify by hand:

vagrant ssh

And start over from a clean machine:

vagrant destroy

Example

My role ansible-role-ntp contains the complete setup if you would like to see it in context.