Vagrant Provisioning with Ansible

Provisioning Virtual Systems with Ansible Local

Vagrant supports provisioning systems with Ansible, either through executing tasks remotely from the host, or through executing tasks locally on the target system.

This tutorial uses the Ansible local provisioner (ansible_local), so that you do not need to install Ansible on the host. Vagrant will handle downloading and installing Ansible on the virtual guest, and then run a specified playbook locally on that virtual guest.

About Ansible

Ansible is a popular remote execution tool that can replace the classical ssh-for-loop patterns. Coupled with change configuration capability, Ansible is great web application deployments or for cloud environments where orchestration is required.

Ansible is an infrastructure as code solution for AWS, Google Cloud, Azure, and other cloud services, and can interact with web interfaces (RESTful and others), and can configure network equipment. Ansible positions itself as a competitor to the shell, driving ease of use and flexibility as an alternative for automation chores.

Ansible’s configuration scripts are called playbooks, that contain a collection of tasks to run on a remote host. Ansible can group together a collection of tasks into a component called a role. A role contains its own local files, templates, variables, and metadata.

Ansible includes an inventory system to classify configurable remote hosts into groups. Ansible supports self executing dynamic inventory, where remote hosts can be dynamically organized by any mechanism of your choosing, e.g. hostname, AWS EC2 tags, GCE, Azure, Serf, Consul, Nagios, Cobbler, etc.

Prerequisites

As this tutorial is about Vagrant, you need to install Vagrant, and along with Vagrant, you need the virtual systems with Virtualbox. It may be possible to use other virtual platforms, but this can be complex, and doesn’t work consistently across macOS, Windows, and Linux.

Additionally, the instructions are work with Curl and the Bash shell. If you do not have this, you need to either run Bash, or convert the commands something similar in your shell.

Optionally, you can run Ansible on the host, but this is not required.

I created some past how-to articles that can get these and other tools on macOS, Windows, and Linux platforms.

Windows 8.1

Using Chocolatey to install the requirements:

macOS High Sierra 10.3

Using Homebrew to install the requirements:

Fedora 28

Using native package manager Dandified YUM, or in short DNF, to install requirements:

Part I: Ansible Role with Intelligent Defaults

We’ll start with an Ansible role called hello_web that will install Apache web server on Ubuntu, and have Vagrant bring up the guest system and run the tasks in the role.

Initial Work Area Structure

Let’s great the staging area or work area for this project.

mkdir -p ~/vagrant-ansible/provision/roles
cd ~/vagrant-ansible
touch Vagrantfile provision/playbook.yml

If you have Ansible installed on the host system, you could run this command to create the role structure:

ansible-galaxy init provision/roles/hello_web

Otherwise, we can create a basic Ansible structure with Bash commands:

mkdir -p provision/roles/hello_web/{defaults,files,tasks}
touch provision/roles/hello_web/{defaults/main.yml,tasks/main.yml}

And we’ll place our HTML content for use later:

cat <<-'HTML' > provision/roles/hello_web/files/index.html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
HTML

Overall this will create a structure like the following:

~/vagrant-ansible                                                       
├── Vagrantfile
└── provision
├── playbook.yml
└── roles
└── hello_web
├── defaults
│ └── main.yml
├── files
│ └── index.html
└── tasks
└── main.yml

Vagrant Configuration

Now we need to create our Vagrant configuration with this Vagrantfile:

Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-16.04"
config.vm.network "forwarded_port", guest: 80, host: 8086
  ####### Provision #######
config.vm.provision "ansible_local" do |ansible|
ansible.playbook = "provision/playbook.yml"
ansible.verbose = true
end
end

The ansible_local provisioner defaults are adequate for our simple role hello_web. The site playbook, or the playbook that runs our desire roles for this guest system is simply called playbook.yml. This will kick off everything we need to make this work.

Create the Playbook

The provisioner picks a playbook to run for the provisioning process. This is where we can select the the hello_web role. Update provision/playbook.yml with this:

---
- hosts: all
gather_facts: yes
become: true
roles:
- hello_web

Create the Role Defaults

Now we can start digging into the role parts. First let’s set some default variables with some intelligent defaults that make sense for a Ubuntu 16.04 Xenial Xerus system.

From within the hello_web role, update default/main.yml with this:

---
hello_web:
docroot: /var/www/html
package: apache2
service: apache2

We’ll reference these variables in our main tasks file for the role.

Create the Role Tasks

From within the hello_web role, update tasks/main.yml with this:

---
- name: "Install Web Service"
package:
name: "{{ hello_web.package }}"
state: present
- name: "Start Web Service"
service:
name: "{{ hello_web.service }}"
state: started
enabled: yes
- name: "Copy Web Content"
copy:
src: "{{ role_path }}/files/index.html"
dest: "{{ hello_web.docroot }}/index.html"

These tasks use three modules (analogous as resources in Chef and Puppet) of package, service, and copy to create our web server and content, and their purpose should be self explanatory based on the name value for each task to describe what the task does.

Inside each task, you’ll notice embedded variables with this notation.{{ variable }}. This from Jinja2 templating system that is heavily used by Ansible. Any value that uses this must be quoted. You can see we referenced the default variables specified earlier.

Testing the Solution

vagrant up                    # download, start guest, provision
curl -i http://127.0.0.1:8086 # access content

This will give us:

HTTP/1.1 200 OK
Date: Sun, 12 Aug 2018 01:06:50 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Sun, 12 Aug 2018 01:06:38 GMT
ETag: "3c-5733296c755b3"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>

Part II: Ansible Role using Extra Variables

Now let’s swap out the guest system for CentOS virtual guest. We’ll have to send some different variables to the hello_web as the role defaults are will not work for CentOS.

Update Vagrant Configuration

Update the Vagrantfile with this content below, which use the CentOS box by the Bento project, and adds some new variables that will be used later by the hello_web role.

Vagrant.configure("2") do |config|
config.vm.box = "bento/centos-7.5"
config.vm.network "forwarded_port", guest: 80, host: 8086
  ####### Provision #######
config.vm.provision "ansible_local" do |ansible|
ansible.playbook = "provision/playbook.yml"
ansible.verbose = true
ansible.extra_vars = {
hello_web: {
package: "httpd",
service: "httpd",
docroot: "/var/www/html"
}
}
end
end

These variables quite readable in this format. If curious, this are in a ruby symbol hash format in the Vagrantfile. This will be converted by Vagrant to an JSON in an escaped shell string used by the ansible-playbook command that Vagrant calls:

\{\"hello_web\":\{\"package\":\"httpd\",\"service\":\"httpd\",\"docroot\":\"/var/www/html\"\}\}

Yuk!!! Anyhow, these extra variables take the highest precedence, and we can add them as needed in a more friendly format.

Testing the Updated Solution

Let’s try the new configuration:

vagrant destroy --force # purge old environment
vagrant up # download, create guest, provision
curl -i http://127.0.0.1:8086

This will show the results for CentOS:

HTTP/1.1 200 OK
Date: Sun, 12 Aug 2018 08:43:09 GMT
Server: Apache/2.4.6 (CentOS)
Last-Modified: Sun, 12 Aug 2018 01:30:35 GMT
ETag: "3c-57332ec6c9bce"
Accept-Ranges: bytes
Content-Length: 60
Content-Type: text/html; charset=UTF-8
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>

Final Thoughts

I hope this helps give a introduction to Ansible and how Vagrant can be used to easily develop and test Ansible playbook and roles.

This only touched the surface of what is capable with Ansible. Vagrant Ansible provisioners have other options, such as using Galaxy roles, Host and Group variables, and more.

Ansible itself can be used to configure and orchestrate more than just systems, but also networking equipment, web interfaces, and cloud platforms.