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
.
ifconfig
is deprecated on Linux now: you can use ip link
to add and remove alias IP addresses instead.