Continuous Testing of Ansible Roles with Docker and Travis CI

Share this :
Share this :

Ansible and Docker

Ansible is one of the de facto standards for IT automation, and this automation is an essential component of modernization and digital transformation. Today, we’ll see how to automate this testing using Travis CI and Docker.

With Ansible we can automate the provisioning, setup and configuration of almost every aspect of our infrastructure in a very simple way.

But testing Ansible is not that straightforward. In Ansible, we can use roles to develop generic functionality and encourage reusability.

There is a central repository, Ansible Galaxy, to share roles with other developers. This central repository is fully integrated with GitHub so you can find many existing roles ready to be used and forked.

These roles may target many Linux distributions at the same time, providing generic functionality for many platforms.

And normally, when developers create a new role they tend to spend a lot of time testing it against fresh infrastructure.

For example, to test a role that installs a new database in any Linux distro, we may need to spin a new Virtual Machine for each distro every time we want to test it, then run Ansible, check it, trash it and start all over again. We may use VM snapshots, but it’s still a terribly slow, and resource hungry, development process.

An alternative approach would be using Docker. Instead of VMs, we could spin a new container every time we want to test our role. Each container would be based on each Linux distro we’d like to support. Although efficient, this will require us to run many steps to make a single test.

What if we were to automate this process into our Continuous Integration pipeline? That’s exactly the approach I’ll explain below.

Ansible CI with Travis

In Travis CI, you can create a CI pipeline where you can run multiple stages in parallel, for a single build, at the same time.

This feature comes in very handy when modeling a continuous testing pipeline for Ansible roles that supports multiple platforms. And given that we would like to share our role via Galaxy and GitHub, it’s pretty convenient too.

I’ve prepared a very basic demo role that installs Nginx on different Linux distributions. It’s pretty simple and we’ll focus on the Travis part. You can find the demo role in GitHub here: https://github.com/drhelius/travis-ansible-demo

To run Docker inside the Travis build, we’ll follow the docs to using Docker in your build and set this in the .travis.yml file accordingly:

sudo: required

services:
  - docker

Now, we’ll create the parallel jobs for every Linux distro, creating a build matrix, like this:

env:
  - distribution: centos
    version: 7
  - distribution: fedora
    version: 26
  - distribution: fedora
    version: 25
  - distribution: fedora
    version: 24
  - distribution: ubuntu
    version: xenial
  - distribution: ubuntu
    version: trusty
  - distribution: debian
    version: stretch
  - distribution: debian
    version: jessie

The build will split into 8 jobs and we’ll use the environment variables defined here for each job to configure the testing for each distro.

To have full control of the specs, we’ll extend the base Docker images in their corresponding Dockerfiles. This way, we can run Ansible, systemd and any other tools you may need when testing your role. You can find my customizations for Ubuntu, Debian, Fedora, … in the travis folder, inside the repository. We’ll use these images based on the official ones.

Back in our .travis.yml file, the first thing is pulling the base image and building our customized version of it:
before_install:
  - 'sudo docker pull ${distribution}:${version}'
  - 'sudo docker build --no-cache --rm --file=travis/Dockerfile.${distribution}-${version} --tag=${distribution}-${version}:ansible travis'

In which ${distribution} and ${version} are the different environment variables we defined in our build matrix. After this, we can create a new container from this image and let it run. To do this we’ll store the container’s ID in a ${container_id} environment variable to use it with the commands following:

script:
  - container_id=$(mktemp)
  - 'sudo docker run --detach --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro --volume="${PWD}":/etc/ansible/roles/nginx_role:ro ${distribution}-${version}:ansible > "${container_id}"'

At this point, our container is up and running at the Travis CI build and we’ll run Ansible in it, to test our role by using a generic playbook test.yml created just for testing purposes.

We start by checking the syntax:

 - 'sudo docker exec "$(cat ${container_id})" env ANSIBLE_FORCE_COLOR=1 ansible-playbook -v /etc/ansible/roles/nginx_role/travis/test.yml --syntax-check'

And continue by running the actual role:

  - 'sudo docker exec "$(cat ${container_id})" env ANSIBLE_FORCE_COLOR=1 ansible-playbook -v /etc/ansible/roles/nginx_role/travis/test.yml'

It’s very convenient to run it twice, so you can detect errors with the idempotence nature of the role:

 - >
    sudo docker exec "$(cat ${container_id})" env ANSIBLE_FORCE_COLOR=1 ansible-playbook -v /etc/ansible/roles/nginx_role/travis/test.yml
    | grep -q 'changed=0.*failed=0'
    && (echo 'Idempotence test: pass' && exit 0)
    || (echo 'Idempotence test: fail' && exit 1)

We can then stop the container and finish our build, at this point everything should be ok:

  - 'sudo docker rm -f "$(cat ${container_id})"'

Now, every time we make a push, or a new PR, this process will trigger and will test our role on the 8 platforms we specified in the matrix, in just a few minutes.

You can check the full .travis.yml file I used here.

About the Author

Ignacio Sánchez, @drhelius works as a DevOps Tech Lead for atSistemas in Spain, helping customers in the automation of the whole development life cycle, implementing CI/CD pipelines and developing infrastructure as code. This post will also be available in Spanish at enmilocalfunciona.io, the atSistemas’ technical blog.

© Copyright 2024, All Rights Reserved