AnsibleXXXIX · Automation Platforms, RBAC and Event-DrivenAutomation platforms, RBAC and event-driven automation
Schedules, workflows and unattended runs
What you'll learn
- Choose which automation is safe to schedule and which is not
- Design a scheduled job whose failure is discovered rather than accumulated
- Build the cron equivalent of a schedule without losing the exit code
- Identify the untested branch in a workflow before it runs during an incident
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
Everything in this part so far has kept a human in the loop. Somebody decided to run something and watched the output.
Schedules remove the human. That is the entire value and the entire risk, and the two are not separable.
The easy case: scheduled jobs that only look
Start with the class of scheduled automation that is straightforwardly a good idea, because it establishes the pattern before the arguments start.
| Scheduled job | Why it wants a schedule | What it changes |
|---|---|---|
| Compliance validation | The answer is only useful if it is current | Nothing |
| Certificate expiry check | Expiry is a date, and dates arrive on their own | Nothing |
| Patch-level reporting | “How far behind are we” is a weekly question | Nothing |
Drift detection with --check | Divergence accumulates silently | Nothing |
| Fact collection into a cache | Inventory questions want fresh facts | Nothing on the fleet |
Every row changes nothing. That is what makes them easy: the worst case for a read-only scheduled job is a wrong answer or no answer, and both are recoverable on Monday.
The hard case: scheduled change
Now the version that gets argued about. Nightly convergence — the schedule that applies the desired state and corrects anything that has drifted.
The case for it is genuinely strong. Drift is corrected before it compounds. The gap between the repository and reality never grows large enough to be frightening. And every run exercises the automation, so it does not rot.
The case against is one sentence: production is being modified while nobody is watching.
Concretely, what “nobody is watching” costs:
Failures are discovered late. A convergence run that starts failing at 03:00 on Sunday is discovered on Monday morning, if somebody reads the right dashboard. Between those two points, drift accumulates unchecked and the estate believes it is converged.
Failures are discovered as a batch. By the time anyone looks, it has failed nine times, and the ninth failure is the only one anybody investigates. The first failure is the one that would have explained it.
A change lands with no observer. The run corrects the drift, restarts the service, and the restart fails on eleven hosts. Nobody is on the terminal. The alert that fires is a service alert at 03:14 from a monitoring system, and the on-call engineer starts debugging a service outage without knowing that automation touched it four minutes ago.
Ownership evaporates. A run somebody launched belongs to that person for the next hour. A run a schedule launched belongs to whoever notices.
Deciding what to schedule
The useful test is not “is this playbook safe”. It is:
- What is the worst thing this can do to a host? Reporting, no change, restart a service, reboot, replace data. Each rung up the ladder needs a stronger reason to be unattended.
- How many hosts can one firing reach? A schedule with no
--limitfires against the whole inventory every time, forever. This is thehosts:line question with the operator removed. - Who finds out, how fast, when it fails? If the answer is “the dashboard, eventually”, it is not ready to be scheduled.
- Does it need a change window, and does the schedule know about windows? A cron expression does not know your business.
The cron equivalent, and the mistake in it
You do not need a platform to schedule automation. You need cron or a systemd timer, and you need to not make the standard error.
Here is the standard error:
0 3 * * * ansible-playbook -i /srv/ansible/inventory/production /srv/ansible/converge.yml
Two problems, and the second is the one that hurts.
The first is that the output goes to cron’s mail, which on most modern hosts goes nowhere at all.
The second is that the exit code is discarded. Ansible is careful about exit codes and this line throws all of that away:
$ ansible-playbook -i inventory.ini compliance.yml; echo "exit=$?"PLAY RECAP *********************************************************************
web01.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
web02.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
web03.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
exit=4Exit 4 — unreachable. Nothing in the cron line above notices. The job
“ran” every night for three weeks and reached nothing, and the only
evidence is a file nobody opened.
A defensible wrapper is not much longer:
#!/usr/bin/env bash
# jobs/nightly-compliance.sh - runs from a systemd timer at 03:00.
set -uo pipefail
LOG=/var/log/ansible/compliance-$(date +%Y%m%d-%H%M%S).log
cd /srv/ansible
ansible-playbook -i inventory/production/hosts.ini playbooks/compliance.yml \
--limit webservers > "$LOG" 2>&1
rc=$?
case "$rc" in
0) notify "compliance run clean" ;;
2) notify "compliance FAILED tasks (rc=2), see $LOG" ;;
4) notify "compliance UNREACHABLE hosts (rc=4), see $LOG" ;;
*) notify "compliance run exited $rc, see $LOG" ;;
esac
exit "$rc"
Note set -uo pipefail without -e: with -e the script would exit at
the ansible-playbook line and never reach the notification, which is the
opposite of what a scheduled job needs. Capturing rc immediately after
the command is the whole point.
Distinguishing 2 from 4 matters more than it looks. Failed tasks mean
your automation or your hosts have a problem. Unreachable means the run
never happened — a different incident, with a different first
investigation, and the one most likely to be silently long-running.
Workflows
A workflow template chains job templates into a graph, where each node branches on the previous node’s result: on success, on failure, or always.
The shape that justifies the feature:
[ pre-flight checks ]
|
on success
|
[ canary: one host ]
|
on success ------------------> on failure
| |
[ rollout: remaining hosts ] [ collect diagnostics ]
| |
on failure [ notify + stop ]
|
[ collect diagnostics ]
That is the canary pattern from Part XXX, expressed as objects rather than as a paragraph in a runbook — with the property that the branch is taken by the system rather than by a person deciding whether the canary looked alright.
Approval nodes pause a workflow until a human with permission approves. This is the mechanism that makes an unattended workflow acceptable at a higher risk level: check and canary run unattended, and the fleet rollout waits for a person.
Surveys parameterise a launch — a form whose answers become extra variables. Everything lesson 2 said about constraining the target set applies here with more force, because a survey is the mechanism by which somebody who has never read the playbook supplies input to it.
Knowledge check
Knowledge check · 4 questions
Q1. A nightly compliance validation job has run from cron for three weeks and the log directory shows a file each night. The team reports compliance as validated. What is the most likely failure this arrangement conceals?
Q2. Which practices make scheduled automation that changes production defensible? Select all that apply.
Q3. A workflow failure branch is well tested in practice, because it runs every time any node in the workflow fails.
Q4. Your incident procedure needs to handle scheduled automation firing during a Sev-1. Which control is most reliable?
Passing score: 75%. Answers are checked in this browser.