Provisioning machines locally with Ansible and 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:
vagrant up
that will instruct Vagrant to create and run the virtual machinevagrant halt
that will stop the virtual machinevagrant destroy
that will destroy the virtual machine (useful when we change the VagrantFile and want to recreate it again)vagrant ssh
that will connect to that machine and give us a remote command line sessionvagrant ssh-config
that will display SSH connection information if we want to connect to the machine without Vagrant
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: Use Ansible integration with Vagrant. Vagrant will call our Ansible playbook on
vagrant up
or onvagrant provision
automatically. - Option 2: Point our Ansible playbook to the IP address of the running Vagrant box. In this case we will invoke our playbook when we need it (e.g. after
vagrant up
is finished).
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.