Testing Ansible roles with LXD and python

One of the sticking points of Ansible at scale, or any similar config management system, is the difficulty in building, testing, and modifying roles in isolation from some persistent environment. A common approach is to duplicate the infrastructure, call that ‘staging’, and run everything through that before moving to production. If you’re running something like kubernetes, openstack, etc at large scale - a small test cluster is a small price to pay to be able to rehearse control plane upgrades. For simpler services - standalone databases, basic web apps, base OS configuration - redundant infrastructure may be less necessary if we have a good way to emulate it.

ansible-mock

ansible-mock is a straightforward bit of python that provisions a --vm or container of --image, generates and lands a throwaway ssh key, and runs whatever is found at ./tasks/main.yml.

Use cases include:

  • developing new roles for hardware or otherwise persistent infrastructure - using an independent docker-like workflow.
  • developing and testing day-2 playbooks like OS upgrades.
  • testing pull requests to ansible roles: provision role/instance on master branch -> switch branches -> run ansible-playbook again.
$ ansible-mock --help
usage: ansible-mock [-h] [--preserve] [--cleanup] [--vm] [--image IMAGE]

options:
  -h, --help     show this help message and exit
  --preserve
  --cleanup
  --vm
  --image IMAGE

It expects to be run from within a role directory, with vars.yml containing default variables if required:

~/git/debian_common$ tree
.
├── handlers
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
│   ├── resolv.conf
│   └── sources.list
└── vars.yml

The inventory, ssh keys, and top-level playbook are generated at runtime.

While it is valuable to be able to rebuild things from scratch, its a good idea to be kind to public repos when iterating on something. If used with --preserve, ansible-mock will leave the vm running and spit out a copy-paste command to simply run ansible again:

$ ansible-mock --preserve --vm
create_node(): creating node ansible-mock-8bda8
wait_until_ready(): waiting for lxd agent to become ready on ansible-mock-8bda8
create_node(): Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  libnsl2 libpython3-stdlib libpython3.11-minimal libpython3.11-stdlib
...
PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [10.178.43.250]
...
PLAY RECAP *********************************************************************
10.178.43.250              : ok=8    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
main(): environment created.  follow-up configuration can be performed with:
ansible-playbook .mock.yml -i .mock/inventory

The result is a workflow quite similar to developing Dockerfiles, but with system(d) containers or vms if needed. If your role has additional non-trivial dependencies you are of course going to have to mock those sort of things up yourself.

When finished, you may have accumulated some hanging instances. You can clean them all up at once:

$ ansible-mock --cleanup
cleanup(): ansible-mock-b9dd7 deleted
cleanup(): ansible-mock-367d4 deleted
cleanup(): ansible-mock-8bda8 deleted

Nathan Hensel

on caving, mountaineering, networking, computing, electronics


2023-02-23