Provisioning machines locally with Ansible and Vagrant

ansible development-tools devops
Using Ansible with Vagrant

Motivation

Vagrant can build and manage virtual machines in a reproducible way using a configuration file. The virtual machines can be run with different providers like VirtualBox and provisioned by software like Ansible. In a nutshell we can easily start and configure a virtual machine locally that mimics our server or other target environment for local development.

Ansible is a powerful automation tool mainly used for configuring and managing remote machines via SSH. We would typically use it to setup our servers and deploy our software in automated way. When we use it together with Vagrant we can easily do the same in a local virtualized environment that can mimic the real one.

Basically the motivation of the article is the ability for us to test Ansible playbooks locally in a separate, but similar environment to the one we target, e.g. to develop automatic server provisioning and application deployment completely locally, on our development machine.

Please read the documentation about these tools for more details, as I will assume that you have a basic knowledge of them. This article is more about how to connect them together.

Prerequisites

The instructions provided here are for Fedora as I use it as my daily driver. The first thing that we have to do is to install Ansible, Vagrant and VirtualBox. We will use VirtualBox because Vagrant can't virtualize machines on its own. Let's start by installing all the packages in Fedora:

sudo dnf install ansible vagrant VirtualBox -y 

Next we need to make sure that virtualization is supported on our host system.

Virtualization support

On Linux systems we can check for Kernel Virtual Machine (KVM) support with:

# check if virtualization is enabled in BIOS
cat /proc/cpuinfo | egrep "vmx|svm"

# check if kernel module KVM is loaded
lsmod | grep kvm

In both cases we should get some output. If nothing is printed, virtualization is not enabled in BIOS or the necessary kernel module is not loaded.

In case we are on Lenovo computers, we can use F1 during the boot to enter BIOS and enable the virtualization under the Security section. Similarly for other computers, as most CPUs do support hardware virtualization. This is important for speed, otherwise we could alternatively use a software virtualization.

To install virtualization support in Fedora, we can install the virtualization package group and start libvirtd service:

sudo dnf install @virtualization
sudo systemctl start libvirtd

# to start the service on every boot
sudo systemctl enable libvirtd

More information about the Fedora virtualization packages can be found in Getting started with virtualization.

Using Vagrant to set up virtual machines

The first things we have to do is to download an image we want to run inside our virtual machine. I will use generic/fedora33 as I want to run a Fedora system. If you want to run something else, look for an image on Vagrant Cloud. When we know what system we want to run, we can download a system image with vagrant box add:

vagrant box add generic/fedora33

Vagrant uses a VagrantFile configuration file written in Ruby that contains instructions on setting up the machines, networks and so on. We can write it from scratch or generate a template with vagrant init, specifying the image file. Let's use it to create a template for us:

vagrant init generic/fedora33

It should create a VagrantFile like this (note that I have removed all comments from the file that contain additional instructions):

Vagrant.configure("2") do |config|
config.vm.box = "generic/fedora33"
end

With the Vagrant configuration file in place, we can use some basic Vagrant commands to create and control the virtual machine:

We might get an error when we want to run the virtual machine with vagrant up if there is no support for virtualization on our host system, e.g. "Error saving the server: Call to virDomainDefineXML failed: invalid argument: could not get preferred machine for /usr/bin/qemu-system-x86_64 type=kvm". If this is the case, go back and first make sure that a virtualization is enabled in the BIOS and Linux kernel.

Using Ansible and Vagrant together

When we can successfuly start our Vagrant box, it is time to setup the provisioning via Ansible. We have two basic options how to use Ansible together with Vagrant:

Option 1: Using Ansible-Vagrant integration

To tell Vagrant that we want to provision the machine with Ansible, we have to modify the VagrantFile:

Vagrant.configure("2") do |config|
config.vm.box = "generic/fedora33"

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

The playbook.yml will then be automatically picked up by Vagrant (use hosts: all in the Ansible playbook).

Option 2: Assign a static IP address to the Vagrant box

Alternatively we can tell Vagrant to assign a static IP address to the virtual machine that we can later reference in our Ansible playbook or inventory file. There are more options on how to do that and the solution might differ when using a different system and/or when we have some different network needs. What worked for me was to configure the public_network type and point Vagrant to the virbr0 network interface (otherwise it tried to use eth0 network interface that is not by default present on Fedora).

So let's modify the VagrantFile again, this time telling Vagrant about the network:

Vagrant.configure("2") do |config|
config.vm.box = "generic/fedora33"

config.vm.network "public_network", ip: "192.168.122.89", :dev => "virbr0", :mode => "bridge", :type => "bridge"

config.vm.hostname = "vagrant.test"
config.ssh.insert_key = false
end

We also need to set config.vm.hostname for the static IP address to work and ssh.insert_key = false so that Vagrant doesn't replace insecure SSH key that we will need for the connection.

After new vagrant up we should be able to ping the machine on 192.168.122.89.

To connect to the machine with Ansible, we will need more than just the address though. We will also need the user name and the associated SSH key. As we saw before, Vagrant offers vagrant ssh-config command that will show us the necessary information. For a single box like ours, the user will be vagrant and the path to the key ~/.vagrant.d/insecure_private_key.

When put together we might arrive at an Ansible inventory file hosts.ini like this:

[vagrant_box]
192.168.122.89

[vagrant_box:vars]
ansible_user=vagrant
ansible_ssh_private_key_file=~/.vagrant.d/insecure_private_key

For Ansible to pick up the inventory file, let's create ansible.cfg:

[defaults]
inventory = hosts.ini

Now that all is done, we can just create our Ansible playbook.

Example Ansible playbook

With both options we have arrived to the point where we need to define our Ansible playbook file, e.g. playbook.yml. Let's install some useful software on our virtual machine as an example:

---
- hosts: all
  become: yes

  tasks:
    - name: "Install packages"
      dnf: "name={{ item }} state=present"
      with_items:
        - nginx
        - certbot
        - postgresql
        - postgresql-server

The hosts is set to all, meaning all available machines will be configured with the playbook. In both Option 1 and Option 2 we have only one target machine, so this playbook will work well with both without modifications. become: yes will make sure that the tasks are executed as root, which is necessary when we want to install additional packages on the system.

There is only one task "Install packages" defined. It uses dnf module to install native packages and with_items syntax to install multiple packages at once.

We can run the playbook with ansible-playbook playbook.yml.

Last updated on 31.1.2021.