AnsibleXXVI · Testing AutomationTesting automation
What you cannot test before production
What you'll learn
- Name the defect classes that no pre-production layer can reach, and why
- Treat the canary as a test with a hypothesis, a window and an abort condition
- Choose a canary host that is representative rather than convenient
- Decide in advance what signals would stop the rollout
- Close the loop by pushing each escaped defect down to a cheaper rung
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
Every layer in this part has been honest about a gap. This lesson is about what remains when you have built all of them properly, because that residue does not go away with more effort — it is structural.
The conclusion the part has been building to: testing is not something you complete. There is no configuration of containers, VMs and staging that reduces the residue to nothing, so the last rung is not a formality performed after testing finished. It is the final test, and treating it as one changes how it is run.
The classes that only exist in production
Load. Not “many requests” — the specific shape of your traffic. A configuration change that adds a few milliseconds per request is invisible at ten requests per second and is an outage at ten thousand, because queues are not linear. Synthetic load is a useful approximation and it is generated by a program that does not behave like your users.
Real data volume and shape. A migration that takes four seconds against a 2 GB extract takes forty minutes against 400 GB, and takes four hours if the production table has an index distribution your extract flattened. Query plans change with statistics; statistics come from real data.
Real hardware. Firmware revisions, NIC offload settings, disk controllers, NUMA topology, and the two machines in the fleet that were bought a year later and have a different CPU stepping. A role that tunes anything hardware-adjacent is tuning for hardware it has not seen.
Real neighbours. Production hosts share things: a hypervisor, a storage array, a switch, a NFS server, a DNS resolver, a rate-limited external API. Behaviour that is fine in isolation can be an incident when forty hosts do it simultaneously — and “simultaneously” is precisely what automation makes possible.
Real concurrent operators. Someone else is deploying. Someone is mid-incident. A scheduled job starts. Your change is not the only thing happening, and the interaction between two safe changes is not guaranteed to be safe.
Time. Certificates expire, log rotation happens at 03:00, the monthly cron collides with your daily one, and the leap in disk usage appears on day nine. A test that runs for ninety seconds cannot observe anything with a period longer than ninety seconds.
The canary as a test
A canary is not “deploy to one host first and hope”. It becomes a test when it has the four things any test has: a hypothesis, a defined observation, a pass condition, and a defined response to failure.
Choose the host deliberately
The convenient canary is the first host in the group, or the one whose name sorts first, or the one somebody knows is idle. All three are selections for irrelevance, and an irrelevant canary is a test with a hypothesis nobody wanted to test.
A representative canary takes real traffic, has real data, and sits on hardware the rest of the fleet resembles. If your fleet is genuinely heterogeneous, the canary is one host per class rather than one host.
# 1. Confirm exactly which hosts this would touch. READ-ONLY.
ansible-playbook -i inventories/production site.yml \
--limit web07.example.com --list-hosts
# 2. Predict the change without making it.
ansible-playbook -i inventories/production site.yml \
--limit web07.example.com --check --diff
# 3. Make it, on one host.
ansible-playbook -i inventories/production site.yml \
--limit web07.example.com --diffStep 1 is not ceremony. --limit takes a pattern, and a pattern that
matches nothing runs against nothing while a mistyped pattern can match
more than intended — --list-hosts is how you find out which of those
you have written, before rather than after.
Define the window before you start
The most common canary failure is not a bad change. It is a canary that was observed for ninety seconds, declared fine, and rolled out — when the defect it was there to catch appears on a memory leak’s timescale, or at the next log rotation, or when the connection pool cycles.
The window is a judgement about the failure modes you are worried about, and it is worth writing down alongside the change:
| Concern | Minimum useful window |
|---|---|
| the service fails to start | seconds |
| the config is wrong in a way requests reveal | minutes of real traffic |
| a resource leak | hours, and a graph |
| log volume or disk fill | one rotation cycle |
| a scheduled job interaction | one period of that job |
| certificate or token expiry | not observable; check the expiry directly |
The last row matters. Some concerns are not observable within any sensible window and must be checked rather than watched.
Decide the abort condition in advance
Written before the change, not derived afterwards from whatever the graphs happen to show.
- Error rate on this host above X for Y minutes.
- Latency at the 99th percentile above Z.
- Any entry matching a specific pattern in the service log.
- Memory above a threshold at the end of the window.
- Anything at all in the log that nobody can explain.
That last one is the one people leave out and the one that catches genuine surprises. “We do not know what that message is” is an abort condition, not a curiosity to investigate later.
Closing the loop
The final habit is what turns this ladder into something that improves rather than something you maintain.
Every defect that reaches production is evidence about your pipeline. For each one, the question from lesson 1 applies: which is the lowest rung that could have caught this, and why did it not run there?
| What escaped | Where it belongs afterwards |
|---|---|
| a mistyped module option | lint rung, with the args rule enabled, or the disposable integration rung |
| a role that was not idempotent | the second-run gate — it was not running, or the task was excluded |
| a service that never started | outcome verification in the scenario, and a systemd-capable environment |
| an assumption about a distribution | the platform matrix, plus that distribution |
| a rollout that took out too much at once | the staging fidelity statement, and an absolute serial |
| a migration that overran the window | not testable pre-production; the fix is a measurement on real data, not a test |
The last row is the honest one. Some findings do not become tests. They become a documented limitation and a different kind of control — a measurement, a monitor, a runbook step — and pretending otherwise produces a test that reassures without covering.
Knowledge check
Knowledge check · 4 questions
Q1. A change is verified in containers, in staging, and on a canary host observed for two minutes. It causes an outage six hours later. Which explanation is most consistent with that timeline?
Q2. Which canary host is the best choice for a change to the web tier configuration?
Q3. Which of these belong in a canary plan, written before the run? Select all that apply.
Q4. Some findings from production should be recorded as documented limitations with a non-test control, rather than converted into a test.
Passing score: 75%. Answers are checked in this browser.