Designing Job Templates That Produce an Audit Trail, Not Just a Run
The tier structure from the last post sorts changes into auto-approve and human-review, and that sort only holds if something enforces it inside the run itself, not in a person’s memory of what blast radius means. That enforcement is a job template problem. Most job templates are built to succeed: the play runs, tasks turn green, the job exits 0. None of that tells you the change was safe to auto-approve, only that it finished.
Pre-check and post-check tasks already exist in most idempotent playbooks, but they only gate execution: a when conditional decides whether to run, and nothing about that decision survives past the task. Register the pre-check result into a fact instead of branching on it, then register a second fact after the change completes, giving the report task a before and after instead of a pass or fail.
--check and --diff are not a debugging convenience here. AAP exposes check mode as a job template setting, but the diff lands in stdout like any other output. A diff nobody extracts from stdout is not an artifact. Pull the diff from the check-mode run and attach it to the change record before anything touches production. A check-mode run reporting no diff on a target flagged for drift is itself a finding: either the drift self-corrected or the detection was wrong. Either way, the job template stops and reports rather than running against a target that no longer matches what triggered it.
A job template that reports every non-success as “failed” erases the distinction that makes the audit trail useful. Drift is the post-check confirming the target still does not match baseline, a policy outcome rather than a task failure. Error is a module failing or a bad task, caught by a rescue block that registers which task failed. Environmental is different: an unreachable host never enters the block, Ansible drops it from the play unless ignore_unreachable: true is set, so it needs its own check instead of folding into rescue logic.
The last task in the play assembles the pre-check fact, the diff, the classification, and the post-check fact into one record. set_stats writes that record into the job’s artifact data, queryable through the AAP API, instead of leaving it to whoever parses stdout. The human-readable summary comes from that same artifact, not a separate log that can drift from what happened.
What an audit-ready job template needs, concretely:
- A pre-check that registers its result into a fact, not a conditional that only gates whether the task runs
- A post-check that registers a second fact after the change, so the record holds a before and after, not a pass or fail
- A check-mode run with the diff extracted from stdout and attached to the change record before the live run executes
- Failure classification that treats unreachable hosts separately from
rescue-caught task errors, since unreachable hosts skip rescue by default - A structured artifact (
set_stats, queryable through the AAP API) that both the report and the human-readable summary generate from