Skip to main content
RunBook Academy

AnsibleXXVI · Testing AutomationTesting automation

One role, three distributions

Intermediate⏱ ~20 min🧪 Lab requiredmoleculepodman

What you'll learn

  • Extend a scenario to a platform matrix without duplicating the scenario
  • Choose between one scenario with several hosts and several scenarios
  • Estimate what a matrix costs in wall-clock time and maintenance
  • Decide which platforms a role supports, and say so in the repository
  • Recognise combinatorial growth before it makes the suite unrunnable

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

The single most valuable thing containers give you is cheap cross-distribution testing. A role written and tested only on Debian will contain assumptions about package names, service names and filesystem layout that its author never noticed making, and those assumptions surface on the day someone points it at a Rocky host.

The matrix is also the easiest part of a test suite to let grow until it stops being run. This lesson covers both halves.

Adding platforms

If the scenario declares its instances as inventory, as in the previous lesson, then extending it is an inventory edit:

Read-only / Safemolecule/default/inventory.yml
---
all:
children:
  molecule:
    hosts:
      ubuntu-2404:
        container_image: docker.io/library/ubuntu:24.04
      debian-12:
        container_image: docker.io/library/debian:12
      rocky-9:
        container_image: quay.io/rockylinux/rockylinux:9
    vars:
      ansible_connection: containers.podman.podman

create.yml and destroy.yml already loop over groups['molecule'], so nothing else changes. molecule converge now applies the role to three containers, and molecule idempotence re-runs it against all three.

That is the whole mechanism. The interesting part is what it finds.

What the matrix actually catches

Run a Debian-developed role against Rocky and the failures come in a predictable order.

Package names. apache2 on Debian and Ubuntu, httpd on RHEL-family. libssl-dev versus openssl-devel. python3-pip exists on all three and installs different things. The fix is a variables file per OS family, and the role loading the right one:

Configuration changeroles/webserver/tasks/main.yml
- name: Load the variables for this OS family
ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"

- name: Install the web server
ansible.builtin.package:
  name: "{{ webserver_package }}"
  state: present

Service names. The same service is apache2 on Debian and httpd on Rocky, and the unit names differ accordingly.

Configuration layout. /etc/apache2/sites-available/ with a2ensite on Debian; /etc/httpd/conf.d/ with no equivalent tool on Rocky. This is the difference that most often forces a role to be restructured rather than parameterised, because the two distributions do not have the same shape of configuration, only the same software.

Default users and groups. www-data versus apache.

What is installed by default. A minimal Rocky container has no python3 until something installs it; a Debian container has no sudo, no iproute2 and often no ca-certificates. These belong in prepare.yml only when your real hosts have them and the image does not — otherwise you are hiding a genuine role gap.

SELinux. Present and enforcing on RHEL-family hosts, absent on Debian. In a container it is usually neither, which is a fidelity gap worth writing down rather than a matrix result.

One scenario or several?

Both shapes work, and they optimise for different things.

One scenario, several hosts — the layout above. The role is applied to all three instances in one converge, and the whole thing passes or fails together.

  • Fastest, because create, converge, idempotence and verify each run once against all hosts, with Ansible’s normal parallelism across them.
  • Simplest to maintain: one set of playbooks.
  • Coarse failure reporting: “the scenario failed” and you read the log to find out which host.
  • Cannot vary the scenario per platform — if Rocky needs a different prepare.yml, you are writing conditionals in a playbook.

One scenario per platformmolecule/ubuntu/, molecule/debian/, molecule/rocky/.

  • Independent: molecule test -s rocky runs one platform, which is what you want when only that one is failing.
  • Can differ structurally — different prepare.yml, different verify assertions, different images for a systemd-enabled variant.
  • More files to keep in step, and the duplication drifts.
  • molecule test --all runs them all, and -e excludes one.

The pragmatic default is one scenario with several hosts for the fast matrix, plus a separate scenario for anything structurally different — typically a systemd scenario using a -ubi-init style image, which needs different container options and therefore a different create.yml.

Read-only / Safetargeting scenarios
$ molecule test --help
  -s, --scenario-name TEXT        Name of the scenario to target.  May be
                                specified multiple times. (default: default)
-e, --exclude TEXT              Name of the scenario to exclude from
                                targeting. May be specified multiple times.
--all / --no-all                Target all scenarios. Overrides scenario-
                                name. For 'reset', this includes shared
                                state and inventory. (default: disabled)
--destroy [always|never]        The destroy strategy used at the conclusion
                                of a Molecule run. (default: always)
--workers TEXT                  EXPERIMENTAL: Number of concurrent worker
                                processes. Accepts an integer, 'cpus', or
                                'cpus-1'. (default: 1)

What it costs

The costs are not evenly distributed and it is worth being specific about which ones bite.

Wall-clock time grows with the slowest platform, not the sum, when the hosts are in one scenario — Ansible runs them in parallel up to forks. Separate scenarios are serial by default: three scenarios take three times as long unless you use --workers, which is still marked experimental.

Image pulls are the surprise. Three base images is three pulls on a cold CI runner, and a Rocky image is substantially larger than a Debian one. Caching the images in the runner is usually worth more than any other optimisation available here.

Maintenance is the cost that actually kills matrices. Every platform you add is a platform that can break for reasons unrelated to your change: a base image that stops shipping a package, a repository that moves, a distribution release that goes end-of-life and its mirrors disappear. A red build nobody caused is the fastest way to teach a team to ignore the build.

Cognitive cost on the role itself. Every distribution difference becomes a conditional, a variables file, or a restructure. Three is usually manageable. Six is a role whose logic is mostly about distributions and only incidentally about the service it configures.

The combinatorics, stated plainly

The matrix has more dimensions than distribution, and they multiply:

DimensionRealistic valuesRunning total
distribution33
role variable combinations that change behaviour412
container and systemd-enabled variants224
VM as well as container248

Forty-eight scenario runs per commit is not a testing strategy, it is a queue. The way out is not to test fewer things — it is to test different things at different frequencies:

  • Every commit: one scenario, three distributions, default variables, containers. Minutes.
  • Nightly: the variable combinations that change behaviour, still in containers.
  • Before a release: the VM scenario, on the platforms you actually ship to.

That schedule keeps the fast feedback fast and the expensive coverage real, and it makes the honest statement easy: this commit was tested against three distributions with default settings, in containers.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A role branches on ansible_facts["distribution"] to pick package names, and has separate cases for Ubuntu and Debian that are identical. What is the better key?

  2. Q2. Your CI matrix is one Molecule scenario with three container hosts. What is the main thing you give up compared with three separate scenarios?

  3. Q3. Which of these are real reasons a platform matrix stops being useful over time? Select all that apply.

  4. Q4. Pinning test images by digest is the safest choice, because it guarantees the test environment never changes.

Passing score: 75%. Answers are checked in this browser.