Testing Ansible playbooks on localhost

How to develop and test Ansible playbooks on your local machine with Docker and some shell scripting.

Ansible playbooks are an extremely powerful tool for managing complex multi-server setups. But when you are developing playbooks, you can not just run them towards your production environment to test them.

In this article we present a method for testing playbooks locally with Docker containers as targets.

Overview

We are going to create several Docker containers and use them as Ansible targets.

We will also alias IP addresses to the loopback interface and expose container SSH ports on those. This is mostly a workaround for macOS because it does not have Docker network interface.

Structure

The setup fits into one folder with a few files, and Docker is the only requirement apart from Ansible.

$ tree ./dev

./dev

├── Dockerfile # Docker image for test containers

├── id_rsa # Private key for ansible to ssh into test targets

├── id_rsa.pub # Public key for test targets

├── inventory # Ansible inventory file with test targets

├── test.sh # Script to run ansible towards test targets

└── vars.yml # Ansible playbook variable overrides

Test inventory

In the dev/inventory file we put the Ansible hosts config with *.ans.local hostnames. This is how it looks in our setup:

$ cat dev/inventory

[all]

s1.ans.local

s2.ans.local

When dev/test.sh is run, we first parse the test inventory file to get hostnames of test targets.

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

INVENTORY_FILE="$DIR/inventory"

TARGETS=$(grep '\.ans\.local' $INVENTORY_FILE | grep -v ';' | sort | uniq)

Docker interface workaround for macOS

Docker on macOS does not have docker0 interface, so you can not access containers via the network from the host without exposing their ports on host IP.

We could expose SSH on 127.0.0.1 with different port for each container, but Ansible does not see the difference between 127.0.0.1:1234 and 127.0.0.1:5555 , so we still need unique hostnames. Another problem with using different ports is that it complicates the Ansible run configuration - we would have to set ansible_port for each target.

To circumvent these limitations, we alias IP addresses to the loopback interface for each hostname from dev/inventory and put the hostname for the alias in /etc/hosts . This way we get unique IP addresses, unique hostnames for Ansible, and the same exposed SSH port on each container.

IP_PREFIX="192.168.200"

setup_test_ips() {

counter=1

for host in $TARGETS

do

ip="$IP_PREFIX.$((counter++))"

sudo ifconfig lo0 alias "$ip"

echo " $ip $host" | sudo tee -a /etc/hosts

done

}

Test containers

We build the Docker image with SSH server and our public test key in it.

DOCKER_IMAGE_NAME="ansible_local_test"

cd $DIR && docker build -t "$DOCKER_IMAGE_NAME:latest" .

Now we can start the Docker containers for each test target. We are going to assign hostname as a name of container to reference them easily when needed. We are exposing port 22 on the corresponding aliased IP in each container.

start_containers() {

counter=1 for host in $TARGETS

do

ip="$IP_PREFIX.$((counter++))"

docker run \

-d \

-p "$ip":22:22 \

--name "$host" \

"$DOCKER_IMAGE_NAME:latest"

done

}

Running Ansible

Now we are all set to run our Ansible playbook towards the test targets.

ID_RSA_FILE="$DIR/id_rsa"

OVERRIDE_VARS_FILE="$DIR/vars.yml"

PLAYBOOK_FILE="$DIR/../playbook.yml"

ANSIBLE_HOST_KEY_CHECKING=False \

ansible-playbook \

--inventory-file="$INVENTORY_FILE" \

--user=root \

--private-key="$ID_RSA_FILE" \

--inventory-file="$INVENTORY_FILE" \

--extra-vars="@$OVERRIDE_VARS_FILE" \

"$PLAYBOOK_FILE"

Cleaning up

After Ansible is done, we remove the IP aliases from the loopback interface and test target hostnames from /etc/hosts .

cleanup_test_ips() {

grep -v "$IP_PREFIX" /etc/hosts | sudo tee /etc/hosts counter=1

for host in $TARGETS

do

ip="$IP_PREFIX.$((counter++))"

sudo ifconfig lo0 -alias "$ip"

done

}

Demo project

I made a demo project that illustrates the approach described in this article. It contains a simple Ansible playbook and setup for testing it locally. You can clone it and follow instructions in README to try it out.

The demo project can be found here: https://github.com/xeneta/ansible-local-dev

Using Linux?

Everything is much easier. You can skip IP setup and cleanup steps, and instead map hostnames to Docker container IP addresses on docker0 interface - you can find them with docker inspect .