Vagrant is able to provision its virtual machines in a number of ways, including using the ‘ansible‘ automation tool. This post looks at vagrant ansible provision and a simple example of how it could be used. As well as being a good tool for provisioning vagrant machines, using vagrant is also a good way to understand how ansible works.
Telling Vagrant To Use Ansible
We need to modify the vagrantfile to specify the provisioner, and where vagrant can find the provisioning configuration file. To keep things simple I am going to use ‘ansible_local‘, a variant of the Vagrant’s ansible provisioner which installs ansible on the guest machine.
Ansible uses configuration files called ‘playbooks‘ and we need to tell vagrant where to find the playbook to provision a machine. A simple example of a vagrant file suitable for vagrant ansible provision is below:
# -*- mode: ruby -*- # vi: set ft=ruby : # encoding: UTF-8 Vagrant.configure("2") do |config| config.vm.box = "ubuntu/trusty64" config.vm.provision "ansible_local" do |ansible| ansible.playbook = "playbook.yml" end config.vm.provider "virtualbox" do |v| v.gui = true end end
The configuration file for ansible provisioning is called a ‘playbook‘. The playbook contains settings for running the ansible provisioning process. I go through an example playbook in the next section, but it is worth knowing a few general principles about playbooks first.
Ansible playbooks are broken down into a series of ‘plays’ – a series of tasks set to be run on a particular machine or group of machines.
Ansible is capable of provisioning multiple machines from one playbook, and not all plays will apply to all host machines. The host option within a play specifies which machines should be affected by a play.
The tasks carried out by a play are what really affect the host machine. A task runs different ansible modules to make changes to the host machine.
Ansible is designed to use different modules. These modules are rather like a wrapper to functionality on the host machine. Ansible modules aren’t ‘magic’, and you will still need to understand the host machine, such as package manager, file structure and so on to use them effectively.
Playbooks are written in ‘yaml‘ – a markup language which is relatively easy for a human to read as it mostly does away with the requirement for opening and closing of tags. The downside to this readability is that it is very sensitive to white space. You may want to take a look at some of the guides out there about correctly formatting yaml files, at least so that you are aware of some of the problems
There is some flexibility in how playbooks can be written. On the one hand this is useful as you can write things in the way that makes the most sense for a given context. On the other hand it can make deciphering playbooks a challenge if you are not familiar with the different possible formats. There are guides to best practice for writing playbooks to help.
An Example Vagrant Ansible Provision
The following example goes through pretty much the same steps as in my previous post about provisioning a vagrant machine with a shell script. (It is actually possible to use ansible to just run a shell script, but then you lose much of the power and flexibility it can bring.)
I use this example file to provision a virtual machine with some software unavailable in package managers. I use the ubuntu ‘apt’ package manager module to install a desktop environment and some dependencies (tcl/tk). I then use the ‘file’ and ‘unarchive’ to download and unpack an archive file from the web. I finally use the ‘command’ module to issue the commands needed to build my software. This example playbook uses a few different modules, and hopefully gives you a sense of how ansible works and how an ansible playbook fits together.
Using the ‘apt’ module
The first few plays use the ‘apt‘ module to install lubuntu-desktop and some other dependencies. The plays have a name which helps identify it within the playbook, as well as in the ansible reporting output. I have included the ‘update_cache’ option in each case – this is the equivalent of running ‘apt-get update‘ to update repository information. For simplicity I have set the ‘hosts’ to be all, although this can be configured to run only on particular machines or groups of machines.
Using the ‘file’ and ‘unarchive’ Modules
I use two more modules to download and unpack my software. Firstly the file module checks that a directory exists, and creates it if not. Then the unarchive module gets an tar.gz at a remote location and unpacks it to the folder I just checked/created with file.
Using the ‘command’ module
My final play includes two tasks which use the command module to run the commands needed to build the software. An important option to include is the chdir within args, to temporarily set the working directory to the one with my software in. Without this the command would be unable to find the scripts it needed, and would fail.
--- # This is a 'play' to install lubuntu-desktop - name: Install lubuntu-desktop hosts: all tasks: - name: Install tcl8.6-dev become: true apt: name: lubuntu-desktop install_recommends: no state: present update_cache: true # This is a 'play' to install tcl/tk - name: Install tcl/tk hosts: all tasks: - name: Install tcl8.6-dev become: true apt: name: tcl8.6-dev state: present update_cache: true - name: Install tk8.6-dev become: true apt: name: tk8.6-dev state: present update_cache: true # This is a 'play' to download and unpack oommf - name: Install oommf hosts: all tasks: - name: Ensure oommf directory exists become_user: vagrant file: path: ~/ state: directory - name: Download and unpack OOMMF become_user: vagrant unarchive: src: http://math.nist.gov/oommf/dist/oommf20a0_20170929.tar.gz remote_src: yes dest: ~/ # This is a 'play' to download and unpack oommf - name: Install oommf hosts: all tasks: - name: Run oommf distclean become_user: vagrant command: tclsh8.6 ./oommf.tcl pimake distclean args: chdir: ~/oommf - name: Run oommf distclean become_user: vagrant command: tclsh8.6 ./oommf.tcl pimake args: chdir: ~/oommf