What AutomationOps is
Link to section “What AutomationOps is”In 30 seconds
Link to section “In 30 seconds”AutomationOps is not a standard term. In this Atlas it names one discipline: running operational automation as a production system in its own right: deciding what to automate, making repeats harmless, limiting how far one run can reach, controlling who or what may trigger it, and keeping an owner until it is retired. Scripts, runbook jobs and auto-remediation rules hold administrator rights and act faster than anyone can intervene; this page is about governing them.
Google’s SRE book gives the reason: “For SRE, automation is a force multiplier, not a panacea”.[1] In what this page calls the Diskerase incident, a decommissioning workflow restarted after a failure correctly found nothing left to erase, but “the empty set was used as a special value, interpreted to mean “everything.”” Almost all of Google’s colocation machines were wiped. Spare capacity limited users to “a slight increase in latency”; repair took the better part of two days, then weeks of new sanity checks.[1]
For engineers who write operational automation, teams that host it, managers who approve it and auditors who must trust it. Code and labs: AutomationOps in practice.
At a glance
Link to section “At a glance”| Item | Summary |
|---|---|
| Term status | Ambiguous. Competing vendor meanings (RPA); no neutral IT-operations definition found (2026-10-16). The practice is long established. |
| Goal | Automation that removes toil without causing outages: safe to run, to repeat and to stop. |
| In scope | Choosing candidates; idempotency; dry runs; target validation; blast-radius limits; batches and failure budgets; approvals; kill switches; runbook platforms; Ansible as an executor; testing; identities, audit, ownership; metrics. |
| Out of scope | IaC lifecycle → InfraOps; durable workflow engines → WorkflowOps; chat as the interface → ChatOps; network automation → NetworkOps; measuring toil → SRE. |
| Prerequisites | Linux and scripting basics (SysOps); the idea of toil (SRE). |
| Key outputs | A register with owners and risk tiers; guarded runners; readiness reviews; run metrics; kill-switch drill records. |
The problem it solves
Link to section “The problem it solves”Organisations fail with operational automation in two opposite ways.
Too little, done by hand. Manual procedures drift. In 2012 a Knight Capital technician “did not copy the new code to one of the eight SMARS computer servers”; no second person reviewed the deployment, and a repurposed flag woke dormant code. Knight built up unwanted positions in about 45 minutes and ultimately lost more than $460 million.[8] For well-scoped procedures, consistency is “in many ways the primary value of automation”.[1]
Too much power, too few brakes. The SRE book notes that “a platform also centralizes mistakes”, meaning a fix made once applies everywhere.[1] The other side, shown by public post-incident reports, is that one bad input also applies everywhere:
| Year | What ran | What went wrong | Added afterwards |
|---|---|---|---|
| 2017 | AWS S3 capacity-removal tool, from a playbook | “one of the inputs to the command was entered incorrectly” | Slower removal; a floor at “minimum required capacity”[9] |
| 2021 | Facebook backbone maintenance command | The audit tool meant to stop it had a bug | Looking for ways to simulate backbone loss in its “storm” drills[10] |
| 2022 | Atlassian deletion script | Site IDs instead of app IDs; 883 sites deleted in 23 minutes | Universal soft deletes, staged rollout[11] |
| 2025 | AWS DynamoDB DNS automation | A race left an empty DNS record “that the automation failed to repair” | Automation disabled worldwide; an NLB velocity control announced[12] |
| Operating reality | Without AutomationOps | With it |
|---|---|---|
| A typo in the target list | Runs against whatever matched | Refused before any change |
| A bad change | Reaches every host at once | Stops after the first failing batch |
| “What did the bot do last night?” | Grep shell histories | Read one audit log |
What AutomationOps is not
Link to section “What AutomationOps is not”- Not “automate everything”. Some work should be removed or stay manual (when not to use); whether operations can disappear is for NoOps.
- Not infrastructure as code (InfraOps), not a workflow engine (WorkflowOps) and not ChatOps, which is one way to trigger automation (ChatOps). This page covers run-time actions: restarts, clean-ups, failovers, rotations, fleet-wide configuration runs.
- Not a way to remove human error. Bainbridge (1983) observed that the designer who tries to eliminate the operator “still leaves the operator to do the tasks which the designer cannot think how to automate”.[13] Errors move to design time and are copied to every run.
Where it comes from and where it sits
Link to section “Where it comes from and where it sits”Origins and evolution
Link to section “Origins and evolution”The label is recent and owned by no community; the practice is old.
| Year | Milestone | What it shows |
|---|---|---|
| 1983 | Bainbridge, “Ironies of Automation”[13] | Automation leaves people the hardest tasks and erodes skills |
| 2013 | xkcd 1205, “Is It Worth the Time?”[14] | The time-saved calculation, later cited by the SRE book |
| 2016, 2018 | Google’s SRE book and Workbook[1][15] | A hierarchy from manual to autonomous; defensive automation |
| 2017–2025 | S3, Facebook, Atlassian and AWS DNS reports[9][10][11][12] | Guards, floors and kill switches added after the fact |
| 2021 onwards | UiPath Automation Ops (generally available April 2021);[3] Robocorp’s “AutomationOps” methodology (archived 2023)[2] | The name appears, in another field (RPA) |
| 2026 | AI operations agents with review and autonomous modes[16][17] | Approval design becomes a product setting |
Glossary and mental model
Link to section “Glossary and mental model”| Term | Meaning on this page |
|---|---|
| Toil | Production work that is “manual, repetitive, automatable, tactical, devoid of enduring value” and grows with the service.[18] |
| Runbook automation | A documented procedure turned into a job others can run (“self-service access”, in Rundeck’s words).[6] |
| Idempotent | “the result of performing it once is exactly the same as the result of performing it repeatedly without any intervening actions”.[19] |
| Dry run / check mode | Reports what would change without changing it; “just a simulation”.[20] |
| Target set, blast radius | What one run will touch; how much it can damage before something stops it. |
| Batch, failure budget | A few targets at a time; halt after a set number of failures (Ansible serial and max_fail_percentage; AWS “rate controls”).[21][22] |
| Guard | A check that refuses or halts a run: validation, caps, floors, health checks, time limits. |
| Velocity limit | A cap on how many actions automation may take, or how much capacity it may remove, per period.[12] |
| Supervised / autonomous | Needs a person’s approval, versus acts and reports. |
| Kill switch | Stops automation at once; feature-flag practice uses the term for switches that “gracefully degrade” functionality.[23] |
| Human-backed interface | A structured request that people still fulfil, a step towards automation.[15] |
Mental model: an automation is a change approved in advance, to be made many times, by someone who is not you, at a time you did not choose. Judge it like that change. Risk is roughly reach × speed × irreversibility; every control here reduces one of them.
How AutomationOps relates to other Ops
Link to section “How AutomationOps relates to other Ops”Diagram source (Mermaid)
flowchart LR S1["SRE: measured toil"] --> AO S2["IncidentOps: recurring fixes"] --> AO S3["ITOps: routine requests"] --> AO RPA["RPA bots: no Atlas page"] -.->|"same controls"| AO AO["AutomationOps: choose, guard, own, retire"] --> N1["InfraOps: IaC lifecycle"] AO --> N2["WorkflowOps: durable engines"] AO --> N3["ChatOps: chat trigger"] AO --> N4["NetworkOps: network change"] classDef core fill:#e8f3f2,stroke:#16756f,color:#122029 classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029 classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029 class AO core class S1,S2,S3,N1,N2,N3,N4 neutral class RPA risk
Text equivalent: SRE (measured toil), IncidentOps (recurring fixes) and ITOps (routine requests) feed work into AutomationOps, which chooses, guards, owns and retires automation and passes its controls to the neighbours that own the mechanics: InfraOps, WorkflowOps, ChatOps and NetworkOps. RPA bots (no Atlas page) need the same controls.
| Neighbour | Shared ground | AutomationOps adds | Go there for |
|---|---|---|---|
| SRE | Toil reduction | Safety and governance of the automation | Defining and capping toil |
| InfraOps | Ansible and other tools | Run-time actions, guards, ownership | Terraform state, drift, modules |
| WorkflowOps | Multi-step jobs, retries | When a simple runner is enough | Durable execution, sagas |
| ChatOps | Commands that change production | What the command may do | Bot design, chat permissions |
| IncidentOps | Auto-remediation, runbooks | Velocity limits, kill switches | Incident roles, MTTA/MTTR |
| PlatformOps | Self-service | Tiers and controls for self-service actions | Building the platform product |
| NetworkOps | Change automation | General automation governance | YANG, source of truth, pre-change verification |
| AIOps | Event-driven triggers | Guarding the action | Correlation and anomaly detection |
| NoOps | Removing manual work | Where the displaced work goes | Whether operations can disappear |
Principles and operating model
Link to section “Principles and operating model”A proposed charter and principles
Link to section “A proposed charter and principles”No AutomationOps manifesto exists. This charter is this Atlas’s proposal, drawn from the SRE books, AWS’s principles and the incidents above.
Mission. Remove toil and speed up routine operations without letting any single run do more damage than a careful person would.
- Safe to repeat. Actions are idempotent, or carry a request identifier so retries have no extra effect.[24]
- Show before doing. A dry run precedes production runs, and someone or something reads it.
- Distrust inputs, “even if that input is flowing from upstream systems”.[15] Empty never means all.
- Small steps. Batches, rate limits and floors; AWS lists “rate control, error thresholds, and approvals” as guardrails.[25]
- Stop fast. A failure budget halts a run; a tested kill switch halts all runs.
- Least privilege, full record. One identity per automation; every run logged.
- Owned or off. Without a current owner, an automation is frozen.
- People stay able. Automation should “default to human operators if it runs into an unsafe condition”,[15] and those people practise the manual path.
Non-goals: automating judgement; maximising the number of automations; removing approvals that add real information.
Building blocks
Link to section “Building blocks”| Block | Job | Without it |
|---|---|---|
| Register | Owner, tier, identity, triggers, review date | Nobody knows what runs or whom to call |
| Executor | Runs the job (script runner, Ansible, Rundeck, cloud runbooks) | Shell histories as the record |
| Guards | Validation, caps, floors, health checks, time limits | One typo reaches everything |
| Approval gate | A second person where the tier requires it | Irreversible actions with no second look |
| Batches, failure budget | Limit reach per step; halt on errors | A bad change reaches the whole fleet |
| Kill switch | Stop one automation, or all | Operators fight the automation mid-incident |
| Identity, audit, tests | Own least-privilege credentials; evidence; early failure | A shared admin key; no record; failure when it matters |
Operating model and governance
Link to section “Operating model and governance”Governance scales with risk. These tiers are this Atlas’s proposal.
| Tier | Typical actions | Required controls | Who may trigger |
|---|---|---|---|
| 0 Read-only | Diagnostics, reports, health snapshots | Rate limits (reads can overload too[15]); audit | Anyone authorised |
| 1 Reversible, single target | Restart a service, clear a cache, renew one certificate | Idempotence, input validation, time limits, audit, kill switch | Self-service or events |
| 2 Reversible, many targets | Fleet configuration, rolling restarts, patch runs | Tier 1 + dry run, target cap, one run at a time, batches, failure budget, health check between batches | On-call or schedule; events only with velocity limits |
| 3 Irreversible or data-destroying | Deletions, decommissions, failovers that lose data, credential revocation | Tier 2 + approval by a second person, soft delete or backup, staged rollout, change record | Named requester plus approver |
Tier-3 runs and changes to automation code map to NIST’s dual-authorization controls (compliance). ChatOps numbers its command tiers differently (2 is any reversible write, 3 any high-impact command); when a chat command starts an automation, apply the stricter tier (editorial rule). Cadence (proposal): metrics weekly; register review quarterly; kill-switch drills twice a year for tiers 2–3.
Roles, ownership and RACI
Link to section “Roles, ownership and RACI”- Automation owner maintains one automation and answers for it; the service owner owns what it touches and agrees its tier.
- Platform team (sometimes an automation centre of excellence) runs executors, register, identities and templates.
- Approver approves tier-3 runs and is never the requester; on-call receives guard trips and may pull the kill switch; security and audit review identities, approvals and logs.
| Activity | Automation owner | Service owner | Platform team | Approver | On-call | Security |
|---|---|---|---|---|---|---|
| Propose and score a candidate | R | A | C | — | C | I |
| Set risk tier and limits | R | A | C | — | I | C |
| Build, test, register | A/R | C | C | — | I | C |
| Promote to autonomous | R | A | C | — | C | I |
| Approve a tier-3 run | I | C | — | A/R | I | I |
| Pull the kill switch | I | I | I | — | A/R | I |
| Quarterly register review | R | A | R | — | — | C |
| Retire | R | A | R | — | I | I |
R = responsible, A = accountable, C = consulted, I = informed. On-call can stop any automation without asking; restarting it needs the owner.
Lifecycle: from candidate to retirement
Link to section “Lifecycle: from candidate to retirement”Diagram source (Mermaid)
stateDiagram-v2 direction TB [*] --> Candidate Candidate --> Rejected: no net value Candidate --> Built: scored and owned Built --> Tested: lint, idempotence, guards Tested --> Supervised: dry runs reviewed Supervised --> Autonomous: promoted Autonomous --> Supervised: demoted Autonomous --> Frozen: owner gone Supervised --> Frozen: owner gone Frozen --> Supervised: new owner Supervised --> Retired: cause fixed at source Autonomous --> Retired: cause fixed at source Frozen --> Retired: nobody needs it Rejected --> [*] Retired --> [*]
Text equivalent: a candidate is rejected (no net value) or built once scored and owned; it is tested, runs supervised while dry runs are reviewed, and is promoted to autonomous with a clean record and a low tier; incidents or repeated guard trips demote it. If its owner leaves it is frozen until re-owned or retired; a supervised or autonomous automation is also retired when the cause is fixed at the source.
Promotion needs evidence: clean supervised runs, guards that tripped correctly in tests, and a tier that allows it. Demotion after an incident should be automatic. The best exit is where Google’s hierarchy ends: a system that “notices problems, and automatically fails over without human intervention”.[1]
Architecture and workflow
Link to section “Architecture and workflow”Reference architecture
Link to section “Reference architecture”This logical architecture fits a team-owned runner, a runbook platform such as Rundeck or a cloud service such as AWS Systems Manager Automation. Guards and the kill switch sit between trigger and executor, and only the executor holds credentials that can change targets.
Diagram source (Mermaid)
flowchart TB
T["Trigger: schedule, alert or person"] --> G["Guards: validate, cap, dry run"]
R["Register: owner, tier, limits"] --> G
G -->|"refused"| H["On-call person"]
G --> A{"Tier 3?"}
A -->|"yes"| P["Second person approves"]
P --> X["Executor with its own identity"]
A -->|"no"| X
K["Kill switch"] -.->|"checked per batch"| X
X --> B1["Batch 1: canary"]
B1 -->|"healthy"| B2["Batches 2 to n"]
B1 -->|"budget exceeded"| H
X --> L["Audit log and metrics"]
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
class R,G,X,L core
class A decision
class K,H risk
class T,P,B1,B2 neutral
Text equivalent: a trigger (schedule, alert or person) enters the guards, which read the register and validate, cap and dry-run the request; refusals go to on-call. Tier 3 needs a second person’s approval before the executor, which has its own identity, runs it. The kill switch is checked before each batch. A healthy canary batch leads to batches 2 to n; an exceeded failure budget goes to on-call. Every run writes to the audit log.
Trust boundaries. Triggers are untrusted input, including your own monitoring. The executor’s identity is the real blast radius: in AWS Systems Manager a run uses either a service role or “the context of the user who invoked the automation”.[26] The executor may append to the audit log but not rewrite it: ship records to a store its identity cannot change (the lab runner’s local file is not tamper-proof).
Alternatives. Google’s hierarchy runs from “an SRE has a failover script in his or her home directory” to systems “that don’t need any automation”.[1] A central runner is the middle step; build very frequent actions into the system itself.
End-to-end workflow
Link to section “End-to-end workflow”Diagram source (Mermaid)
sequenceDiagram
participant T as Trigger and on-call
participant R as Runner
participant S as Targets
T->>R: Target list
Note over R: Guards: validate, cap, dry run
Note over R: Tier 3: second person decides
alt Refused by a guard
R-->>T: Refused, on-call paged
else Rejected by the approver
R-->>T: Stopped, nothing changed
else Accepted
loop Each batch, if switch off and budget left
R->>S: Apply batch
S-->>R: Results, health
end
alt Budget, health or kill switch tripped
R-->>T: Halted, on-call paged
else All batches healthy
R-->>T: Done, audit written
end
end
Text equivalent: the trigger sends a target list to the runner (guards plus executor), which validates, caps and dry-runs it; for tier 3 a second person approves or rejects. A guard refusal pages on-call; a rejection stops the run with nothing changed. Otherwise the runner loops over the batches, checking the kill switch and failure budget before each, and reads results and health. A tripped budget, health check or kill switch halts the run and pages on-call; otherwise it finishes and writes the audit record.
The exception path matters most: every refusal and halt reaches a person with the run id, targets touched and reason.
Practices, frameworks and tools
Link to section “Practices, frameworks and tools”Core practices and playbooks
Link to section “Core practices and playbooks”| Practice | Owner | Verification |
|---|---|---|
| Score candidates | Owner, service owner | Net hours and tier recorded |
| Make it idempotent | Owner | Second run changes nothing |
| Dry run first | Owner | Plan reviewed or checked |
| Validate targets | Owner | Bad-input tests pass |
| Batch with a failure budget | Owner | Halt seen in a test |
| Gate by tier | Platform team | Approvals in the log |
| Kill switch | Platform team | Drill record |
| Test automation code | Owner | Lint, unit, idempotence in CI |
| Own and retire | Platform team | Quarterly review |
Choosing candidates. Start from the toil log (SRE), not from what is most annoying, and estimate net hours and risk tier (Lab 1). xkcd’s chart shows how long you can spend speeding up a routine task before losing time over five years: 5 minutes off a daily task justifies about 6 days, counted around the clock (some 150 hours).[14] It ignores maintenance and risk; the SRE book adds that automation “provides more than just time saving”.[1] For messy requests, start with a human-backed interface.[15]
Idempotency. Write “ensure state”, not “do action”: ensure the service is running, not start it. Where an action cannot be idempotent (create, charge, send), use a caller-supplied request identifier recorded atomically with the change.[24] Google’s idempotent fixes could run “every 15 minutes without fearing damage”, but “not all fixes were naturally idempotent”.[1]
Dry runs, and their limits. In Ansible’s check mode, modules without support “report nothing and do nothing”, and Ansible “will
not generate output for tasks that use conditionals based on registered variables (results of prior tasks)”.[20] The trap is
a read-only probe written as command or shell: check mode skips it unless it uses creates or removes, so a later when: has
nothing real to test.[27] Mark such probes check_mode: false; modules such as find already run in check
mode.[28] A clean dry run is evidence, not proof: list targets with before/after values (diff mode) and check the count.
Validating targets. Diskerase, S3 and Atlassian were input failures: an empty set read as “everything”, a mistyped input, IDs of the wrong type.[1][9][11] Refuse empty lists; resolve every identifier and check its type; cap the count; keep a floor; show people names, not IDs. Review the data as well as the code: Atlassian’s review “focused on which endpoint was being called and how”.[11]
Batches and failure budgets. Ansible aborts when max_fail_percentage is “exceeded, not equaled”.[29] AWS Systems Manager
lets running invocations finish after its error threshold and suggests concurrency 1 if the limit must hold; its user guide and CLI
reference disagree on whether 10% of 50 stops at the fifth or sixth error, so test the semantics.[22][30]
Kill switches, time limits and locks. Keep one switch per automation and a global one, independent of what the automation manages: Facebook’s out-of-band access failed with its backbone.[10] AWS engineers disabled automatic NLB failover mid-incident and later “disabled the DynamoDB DNS Planner and the DNS Enactor automation worldwide”.[12] Safeguards “might be as simple as command timeouts”:[15] give every action a time limit, and take a lock so two runs cannot interleave.
Testing automation code. ShellCheck for shell;[31] ansible-lint, whose no-changed-when rule flags command and shell
tasks that cannot report change;[32] Molecule, whose default sequence includes idempotence.[33] Test each guard with
the bad input it exists for (Lab 2, step 11): Facebook’s audit tool had the bug that let the command
through.[10]
Methods, standards and frameworks
Link to section “Methods, standards and frameworks”| Source | Kind | Contribution |
|---|---|---|
| Google SRE book and Workbook[1][15] | One company’s practice | Value and hierarchy of automation; defensive automation |
| AWS Well-Architected, operational excellence[25] | Vendor recommendation | “Safely automate where possible” |
| NIST SP 800-53 Rev. 5[34] | Control catalogue, binding only where adopted | Change control, dual authorization, least privilege |
| Bainbridge (1983)[13] | Human-factors research | Skills decay; takeover is hard |
| DORA metrics[35] | Research programme | Change fail rate, adapted below |
None defines “AutomationOps”.
Tooling landscape
Link to section “Tooling landscape”| Category | Representative options (verified, October 2026) | Strengths | Limits and risks | Choose by |
|---|---|---|---|---|
| Configuration management and ad hoc execution | Ansible (check mode, serial, max_fail_percentage)[20][21]; Puppet, whose new binaries moved to a private repository under a EULA from early 2025, with the OpenVox community fork[36][37] |
Idempotent modules, large ecosystems | Shell tasks escape idempotence; licence and packaging changes | Module coverage; licence terms; test tooling |
| Runbook automation platforms | Rundeck (Apache 2.0) and its commercial edition Runbook Automation, part of PagerDuty[6]; StackStorm (event-driven, latest GitHub release 3.9.0, October 2025)[38] | Self-service jobs, access control, logs | Another privileged system to run and patch; check project activity | Access model; audit export; maintenance status |
| Event-driven automation | Event-Driven Ansible rulebooks with throttle and host limits[39][40]; StackStorm rules |
Fast response to alerts | Remediation storms without throttles; open webhooks | Throttling, host limiting, source authentication |
| Cloud runbook services | AWS Systems Manager Automation (rate controls, aws:approve)[22][41]; Azure Automation (PowerShell and Python runbooks, Hybrid Runbook Worker)[42] |
Native identities, no servers | Provider lock-in; pricing changed (the Automation free tier ended in 2025)[43] | Where your targets live |
| Linters and tests | ShellCheck[31], ansible-lint[32], Molecule[33] | Cheap, early feedback | Do not prove production safety | Fit with CI |
Automation and AI
Link to section “Automation and AI”AI operations agents are automation whose plan is generated at run time, so these controls apply with more force. AWS describes its DevOps Agent (generally available 31 March 2026) as one of a class of “autonomous systems that work independently”.[16] Azure SRE Agent’s documentation says “Review is the default mode”, yet its table lists Autonomous as the default for response plans and scheduled tasks: check each plan. In Review mode only Azure infrastructure operations get approval buttons; other actions “proceed based on the agent’s reasoning and your response plan instructions”. Approval and permissions are separate. An agent lacking a permission “requests temporary access through Microsoft Entra OBO flow”,[17] so one privileged person’s consent widens its reach. Decide who may grant it.
Rules: give an agent the narrowest identity of any automation; start read-only; route its actions through the same guards and tiers; judge it by the metrics. Correlation and anomaly detection belong to AIOps.
AutomationOps in practice
Link to section “AutomationOps in practice”Reference implementation
Link to section “Reference implementation”Assumptions. A team automates a clean-up of temporary files on application servers and a few riskier actions, with the same controls whatever the executor. All names, accounts and hosts are placeholders.
1. Register entry (one file per automation, reviewed like code). Field names are this Atlas’s proposal.
Example status: YAML syntax-checked; a template, nothing to execute.
id: cleanup-tmp
purpose: Remove temporary files older than 7 days from app servers
owner: team-checkout-ops # a team, plus a named person in your directory
service_owner: checkout
# tier: 0 read-only, 1 reversible single target, 2 reversible many, 3 irreversible
tier: 2
triggers: [schedule, on-call]
executor: ansible # or rundeck, ssm-automation, python-runner
identity: automation-cleanup-tmp # its own identity; never a shared admin key
permissions: ["delete files under /var/tmp/app on hosts in group app_servers"]
limits:
max_targets_per_run: 20
batches: [1, "25%"]
max_fail_percentage: 0
kill_switch: /etc/automation/KILL-cleanup-tmp
dry_run_required: true
approval: none # tier 3 needs a second person
tests: [ansible-lint, molecule-idempotence, guard-tests]
fallback: page on-call; manual procedure RB-112
review_by: 2027-01-16
# status: candidate, built, tested, supervised, autonomous, frozen, retired
status: supervised
2. A guarded runner with every control, in standard-library Python; its “fleet” is a directory of JSON files, so it is safe anywhere (Lab 2). The lock and time limit are Unix-only, and the lock covers one machine.
Example status: verified by execution (Python 3.8.10, Linux, 2026-10-16) with the Lab 2 fleet.
#!/usr/bin/env python3
"""Guarded runner: one operational action with the safety controls built in.
Dry run by default; one run at a time (lock file); refuses empty, unknown,
wrong-kind, duplicate or oversized target lists; works in batches with a failure
budget, a time limit per target and a health check after each batch; checks a
kill switch before every batch; appends one JSON line per decision to an audit
log. Standard library only (Python 3.8+, Linux or macOS). The "fleet" is a
directory of JSON files, so nothing real changes.
"""
import argparse
import fcntl
import getpass
import json
import os
import signal
import sys
import time
import uuid
EXIT_OK, EXIT_REFUSED, EXIT_HALTED, EXIT_KILLED = 0, 2, 3, 4
def load_fleet(fleet_dir):
fleet = {}
for name in sorted(os.listdir(fleet_dir)):
if name.endswith(".json"):
with open(os.path.join(fleet_dir, name)) as f:
host = json.load(f)
fleet[host["id"]] = host
return fleet
def check_targets(targets, fleet, kind, max_targets):
"""Return a list of reasons to refuse; an empty list means the targets are fine."""
if not targets:
# An empty list must never be read as "everything" (the Diskerase lesson).
return ["target list is empty: nothing to do, refusing rather than guessing"]
problems = []
unknown = [t for t in targets if t not in fleet]
if unknown:
problems.append("unknown targets: " + ", ".join(unknown))
wrong = [t for t in targets if t in fleet and fleet[t]["kind"] != kind]
if wrong:
# Right IDs of the wrong type are the classic deletion accident.
problems.append("targets are not of kind '%s': %s" % (kind, ", ".join(wrong)))
if len(targets) > max_targets:
problems.append("%d targets exceed the blast-radius cap of %d"
% (len(targets), max_targets))
if len(set(targets)) != len(targets):
problems.append("duplicate targets in the list")
return problems
def on_timeout(signum, frame):
raise TimeoutError("time limit exceeded")
def cleanup(host):
"""Idempotent action: remove temporary files. Returns (changed, before, after)."""
time.sleep(host.get("hang", 0)) # a real action can hang; the time limit stops it
if host.get("fail"):
raise RuntimeError("simulated failure on " + host["id"])
before = host["tmp_files"]
host["tmp_files"] = 0
return before != 0, before, 0
def healthy(host):
"""Health check after a batch; a real one would probe the service."""
return host.get("healthy", True)
def audit(path, record):
# Append mode only. A local file is not tamper-proof: in production, ship
# these lines to a store the runner's identity cannot rewrite.
with open(path, "a") as f:
f.write(json.dumps(record, sort_keys=True) + "\n")
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--fleet", required=True, help="directory of <host>.json files")
p.add_argument("--targets", required=True, help="file with one target id per line")
p.add_argument("--kind", default="app",
help="the only kind of target this action may touch")
p.add_argument("--apply", action="store_true", help="make changes (default: dry run)")
p.add_argument("--max-targets", type=int, default=5, help="blast-radius cap per run")
p.add_argument("--batch", type=int, default=2, help="targets per batch")
p.add_argument("--max-failures", type=int, default=0,
help="failures tolerated before halting")
p.add_argument("--timeout", type=int, default=10, help="seconds allowed per target")
p.add_argument("--kill-switch", default="KILL",
help="if this file exists, stop before the next batch")
p.add_argument("--lock", default="runner.lock",
help="lock file that allows one run at a time")
p.add_argument("--audit", default="audit.jsonl",
help="JSON-lines audit log, opened in append mode")
a = p.parse_args(argv)
run_id = uuid.uuid4().hex[:8]
mode = "apply" if a.apply else "dry-run"
base = {"run": run_id, "actor": getpass.getuser(), "action": "cleanup", "mode": mode}
lock = open(a.lock, "w") # held until the process exits
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
print("REFUSED: another run holds the lock " + a.lock)
audit(a.audit, dict(base, result="refused", reasons=["locked"], ts=time.time()))
return EXIT_REFUSED
fleet = load_fleet(a.fleet)
with open(a.targets) as f:
targets = [line.strip() for line in f
if line.strip() and not line.startswith("#")]
problems = check_targets(targets, fleet, a.kind, a.max_targets)
if problems:
for msg in problems:
print("REFUSED: " + msg)
audit(a.audit, dict(base, result="refused", reasons=problems, ts=time.time()))
return EXIT_REFUSED
signal.signal(signal.SIGALRM, on_timeout)
failures = 0
batches = [targets[i:i + a.batch] for i in range(0, len(targets), a.batch)]
for n, batch in enumerate(batches, 1):
if os.path.exists(a.kill_switch):
print("KILLED: kill switch %s present before batch %d; %d batch(es) not run"
% (a.kill_switch, n, len(batches) - n + 1))
audit(a.audit, dict(base, result="killed", batch=n, ts=time.time()))
return EXIT_KILLED
for t in batch:
host = fleet[t]
rec = dict(base, target=t, batch=n, ts=time.time())
if not a.apply:
would = "would change" if host["tmp_files"] else "unchanged"
print("%-8s %-7s %s (tmp_files=%d)" % (mode, t, would, host["tmp_files"]))
audit(a.audit, dict(rec, result=would))
continue
try:
signal.alarm(a.timeout)
try:
changed, before, after = cleanup(host)
finally:
signal.alarm(0)
with open(os.path.join(a.fleet, t + ".json"), "w") as f:
json.dump(host, f, indent=2)
result = "changed" if changed else "unchanged"
print("%-8s %-7s %s (tmp_files %d -> %d)"
% (mode, t, result, before, after))
audit(a.audit, dict(rec, result=result, before=before, after=after))
except (RuntimeError, TimeoutError) as e:
failures += 1
print("%-8s %-7s FAILED: %s" % (mode, t, e))
audit(a.audit, dict(rec, result="failed", error=str(e)))
if failures > a.max_failures:
print("HALTED: %d failure(s) exceed the budget of %d after batch %d"
% (failures, a.max_failures, n))
audit(a.audit, dict(base, result="halted", batch=n, failures=failures,
ts=time.time()))
return EXIT_HALTED
sick = [t for t in batch if a.apply and not healthy(fleet[t])]
if sick:
print("HALTED: health check failed after batch %d: %s" % (n, ", ".join(sick)))
audit(a.audit, dict(base, result="halted", batch=n, unhealthy=sick,
ts=time.time()))
return EXIT_HALTED
print("DONE: %s on %d target(s), %d failure(s), run %s"
% (mode, len(targets), failures, run_id))
return EXIT_OK
if __name__ == "__main__":
sys.exit(main())
3. The same controls in Ansible. With serial, the whole play, guards included, runs per batch; any_errors_fatal stops all
hosts if one guard fails;[21][29] ansible_play_hosts_all lists “all the hosts that were targeted by the
play”.[44] timeout limits each task action;[45] with a budget of 0, the final health check stops the play on any
unhealthy host. With several controllers, use a shared kill-switch flag.
Example status: YAML syntax-checked; not executed (no Ansible here). ansible.builtin modules only.
- name: Clean temporary files on app servers (guarded)
hosts: app_servers
gather_facts: false
serial: [1, "25%"] # one canary host, then quarters of the target set
max_fail_percentage: 0 # any failed host in a batch aborts the rest of the play
vars:
max_targets: 20
kill_switch: /etc/automation/KILL-cleanup-tmp
pre_tasks:
- name: Guards (a failure here stops every host)
any_errors_fatal: true
block:
- name: Refuse an oversized target set
ansible.builtin.assert:
that: ansible_play_hosts_all | length <= max_targets
fail_msg: >-
{{ ansible_play_hosts_all | length }} hosts targeted;
the cap is {{ max_targets }}
- name: Read the kill switch on the control node
ansible.builtin.stat:
path: "{{ kill_switch }}"
delegate_to: localhost
run_once: true
register: killswitch
- name: Stop before this batch if the kill switch is present
ansible.builtin.assert:
that: not killswitch.stat.exists
fail_msg: "Kill switch {{ kill_switch }} is present"
tasks:
- name: Find temporary files older than 7 days
ansible.builtin.find:
paths: /var/tmp/app
age: 7d
register: old_files
- name: Remove them (absent stays absent, so a second run changes nothing)
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_files.files }}"
loop_control:
label: "{{ item.path }}"
timeout: 60
- name: Health check before the next batch (placeholder endpoint)
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:8080/healthz"
delegate_to: localhost
register: health
retries: 3
delay: 10
until: health.status == 200
4. AWS Systems Manager. AWS-RestartEC2Instance does not reboot: it stops, then starts, each
instance.[46][47] A stop/start loses RAM and instance-store data. It also changes an automatically assigned public
IPv4 address unless an Elastic IP is attached (as of October 2026).[48] For a reboot, call RebootInstances from your own
runbook. The script refuses an empty or oversized set and passes exactly the running instances it counted. Targeting the tag could
also catch stopped instances, which the run would start (our inference; AWS warns only that “terminated instances might be
included”).[49] The run works one instance at a time and halts at the first error.[30]
Example status: bash syntax-checked; run against a stub aws (empty, normal, oversized, malformed lists), not against AWS.
# Stop and start (not reboot) every RUNNING instance tagged app=checkout, one at a time.
# The run acts on exactly the IDs that were counted, not on the tag again.
set -euo pipefail
CAP=10
ROLE=arn:aws:iam::111122223333:role/automation-restart-checkout
raw=$(aws ec2 describe-instances \
--filters "Name=tag:app,Values=checkout" "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].InstanceId' --output text)
mapfile -t ids < <(tr -s '[:space:]' '\n' <<< "$raw" | sed '/^$/d')
if (( ${#ids[@]} == 0 || ${#ids[@]} > CAP )); then
echo "REFUSED: ${#ids[@]} running instances selected; allowed 1 to $CAP" >&2
exit 2
fi
for id in "${ids[@]}"; do
if [[ ! $id =~ ^i-[0-9a-f]{8,17}$ ]]; then
echo "REFUSED: not an instance ID: $id" >&2; exit 2
fi
done
echo "Stop/start, one at a time: ${ids[*]}"
aws ssm start-automation-execution \
--document-name AWS-RestartEC2Instance \
--target-parameter-name InstanceId \
--targets "Key=ParameterValues,Values=$(IFS=,; echo "${ids[*]}")" \
--max-concurrency 1 \
--max-errors 0 \
--parameters "AutomationAssumeRole=$ROLE"
No IAM action means “restart”: the role needs ec2:StopInstances and ec2:StartInstances, so it could also leave instances
stopped. Scope both by resource tag, as in AWS’s example policies,[50] and test this starting-point policy outside
production. Encrypted volumes may need the KMS actions AWS lists for EBS encryption, such as kms:CreateGrant;[51] an instance
lacking “the required KMS key permissions” can return to stopped during a start.[52] Whoever holds iam:PassRole for
the role can use it.[53]
Example status: JSON syntax-checked; not executed.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "StopStartCheckoutOnly",
"Effect": "Allow",
"Action": ["ec2:StopInstances", "ec2:StartInstances"],
"Resource": "arn:aws:ec2:*:111122223333:instance/*",
"Condition": {"StringEquals": {"aws:ResourceTag/app": "checkout"}}
},
{
"Sid": "ReadInstanceState",
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
}
]
}
A tier-3 runbook adds a read-only plan, a second person and input patterns. aws:approve waits for approvers (default 7 days,
maximum 30; up to 10 approvers; no multi-account or multi-Region runs); with allowedPattern, bad input means “the execution fails
to start”.[41][54] An approver can be a role, so anyone who can assume tenant-deletion-approvers can approve: let
its trust policy name approvers only. tenantctl is a placeholder.
Example status: YAML syntax-checked; not executed.
description: Soft-delete one tenant after second-person approval (Atlas example)
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
AutomationAssumeRole:
type: String
description: Role used only by this runbook
InstanceId:
type: String
allowedPattern: '^i-(?:[a-f0-9]{8}|[a-f0-9]{17})$'
TenantId:
type: String
# This pattern is what makes the substitution into shell commands below safe.
allowedPattern: '^tnt-[a-z0-9]{8}$'
mainSteps:
- name: plan
action: aws:runCommand
onFailure: Abort
inputs:
DocumentName: AWS-RunShellScript
InstanceIds:
- '{{ InstanceId }}'
Parameters:
commands:
- /opt/tenantctl/bin/tenantctl show --tenant '{{ TenantId }}' # read-only
- name: approve
action: aws:approve
timeoutSeconds: 3600
onFailure: Abort
inputs:
NotificationArn: arn:aws:sns:eu-west-1:111122223333:Automation-approvals
Message: 'Soft-delete tenant {{ TenantId }}? Read the output of step "plan" first.'
MinRequiredApprovals: 1
Approvers:
- arn:aws:iam::111122223333:role/tenant-deletion-approvers
- name: softDelete
action: aws:runCommand
onFailure: Abort
inputs:
DocumentName: AWS-RunShellScript
InstanceIds:
- '{{ InstanceId }}'
Parameters:
commands:
- >-
/opt/tenantctl/bin/tenantctl soft-delete --tenant '{{ TenantId }}'
--retain-days 30
5. Event-driven remediation with a velocity limit. Three settings keep one alert from reaching the fleet. First, a host
limit: ansible-rulebook limits a playbook to a host only through event.meta.hosts. The webhook does not set it, so a filter
copies the alerting host there.[40] Second, a throttle: it acts on the first event and suppresses repeats per host until
the window expires.[39] Third, a fleet cap: per-host throttles do not cap the total, so the playbook refuses more than one host
and counts restarts fleet-wide. Actions run one at a time by default, so runs do not race on the count.[55]
The webhook binds to 0.0.0.0 by default, and its token and HMAC checks are optional:[56] without them, anyone who can reach the port can fake an alert and restart workers. Bind to loopback behind an authenticating proxy and set a token. The builtin sources are documented “for testing and development”; check that your production source has the same settings.
Example status: YAML syntax-checked; not executed (needs ansible-rulebook and a workers inventory group).
# worker-rulebook.yml: run with ansible-rulebook -r worker-rulebook.yml -i inventory.yml
- name: Restart a stuck worker on the alerting host only
hosts: workers
sources:
- eda.builtin.webhook:
host: 127.0.0.1 # loopback; an authenticating proxy sits in front
port: 5000
token: "{{ webhook_token }}" # Bearer token from a secret store
filters:
- eda.builtin.insert_hosts_to_meta:
host_path: payload.host # sets event.meta.hosts, which limits the playbook
rules:
- name: Worker stuck, at most once per host per 15 minutes
condition: >-
event.payload.alert == "worker_stuck" and
event.payload.status == "firing" and
event.payload.host is match("worker-[0-9]{2}$")
throttle:
once_within: 15 minutes
group_by_attributes:
- event.meta.hosts
action:
run_playbook:
name: playbooks/restart-worker.yml
# playbooks/restart-worker.yml: started by the rulebook, limited to one host
- name: Restart one stuck worker (guarded)
hosts: workers
gather_facts: false
vars:
fleet_cap_per_hour: 3
marker_dir: /var/lib/automation/restart-worker # on the rulebook host
stamp: "{{ now(utc=true, fmt='%Y%m%dT%H%M%S') }}"
pre_tasks:
- name: Guards (a failure here stops the run)
any_errors_fatal: true
block:
- name: Exactly one host, never the whole group
ansible.builtin.assert:
that: ansible_play_hosts_all | length == 1
fail_msg: "{{ ansible_play_hosts_all | length }} hosts targeted; expected 1"
- name: Make sure the marker directory exists
ansible.builtin.file:
path: "{{ marker_dir }}"
state: directory
mode: "0750"
delegate_to: localhost
- name: Count restarts anywhere in the fleet during the last hour
ansible.builtin.find:
paths: "{{ marker_dir }}"
age: -1h
delegate_to: localhost
register: recent
- name: Fleet-wide velocity limit; beyond it a person decides
ansible.builtin.assert:
that: recent.matched < fleet_cap_per_hour
fail_msg: "{{ recent.matched }} restarts in the last hour: cap reached"
tasks:
- name: Restart the worker service
ansible.builtin.service:
name: worker
state: restarted
become: true
timeout: 120
- name: Record the restart for the fleet-wide count
ansible.builtin.file:
path: "{{ marker_dir }}/{{ stamp }}-{{ inventory_hostname }}"
state: touch
mode: "0640"
delegate_to: localhost
| Control | Guarded runner | Ansible | SSM Automation | Event-Driven Ansible |
|---|---|---|---|---|
| Dry run | Default | --check --diff[20] |
None generic; an Interactive execution mode exists[30] |
Sample events |
| Target validation | Existence, kind, empty, duplicates | assert |
allowedPattern; count, then pass IDs |
Condition; host limit; one-host assert |
| Batches, failure budget | --batch, --max-failures, --timeout, lock |
serial, max_fail_percentage, timeout |
--max-concurrency, --max-errors |
throttle[39]; fleet cap in the playbook |
| Approval | CI approval | Controller approval | aws:approve |
None built in |
| Kill switch | KILL file per batch |
Flag per batch | Remove triggers; deny the role | Disable the activation or stop ansible-rulebook; then enabled: false on the rule[57][58] |
| Audit | JSON lines | Callback or job logs | Execution history | Rulebook output to your log store |
Hands-on labs
Link to section “Hands-on labs”Lab 1 (beginner): score your automation candidates
Link to section “Lab 1 (beginner): score your automation candidates”Objective: rank candidates by net hours and risk tier. Prerequisites: Python 3.8+; no accounts, cost or risk; 30 minutes.
Example status: verified by execution (Python 3.8.10, 2026-10-16; re-run 2026-10-17). The candidate figures are illustrative.
#!/usr/bin/env python3
"""Score automation candidates: net hours returned, break-even and a risk tier.
Input: a JSON list of candidates (see candidates.json). Standard library only.
The tiers and thresholds are this Atlas's proposal, not a standard.
"""
import json
import sys
def tier(c):
if c["read_only"]:
return 0
if not c["reversible"] or c["deletes_data"]:
return 3
return 2 if c["max_targets"] > 1 else 1
def score(c):
manual = c["runs_per_month"] * c["manual_minutes"] / 60
residual = c["runs_per_month"] * c["residual_minutes"] / 60
net = manual - residual - c["maintenance_hours_per_month"]
t = tier(c)
if net <= 0:
advice = "only for consistency or safety"
elif c["needs_judgement"]:
advice = "human-backed interface first"
elif t == 3:
advice = "automate with approval and soft delete"
else:
advice = "automate; supervised, then promote"
months = c["build_hours"] / net if net > 0 else None
return manual, net, months, t, advice
def main(path):
with open(path) as f:
candidates = json.load(f)
print("%-20s %8s %6s %9s %4s %s"
% ("candidate", "manual_h", "net_h", "breakeven", "tier", "advice"))
for c in sorted(candidates, key=lambda c: -score(c)[1]):
manual, net, months, t, advice = score(c)
be = "%.1f mo" % months if months else "never"
print("%-20s %8.1f %6.1f %9s %4d %s" % (c["name"], manual, net, be, t, advice))
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "candidates.json")
[
{"name": "restart-stuck-worker", "runs_per_month": 40, "manual_minutes": 15,
"residual_minutes": 1, "maintenance_hours_per_month": 1.5, "build_hours": 40,
"read_only": false, "reversible": true, "deletes_data": false, "max_targets": 1,
"needs_judgement": false},
{"name": "tls-cert-renewal", "runs_per_month": 6, "manual_minutes": 45,
"residual_minutes": 5, "maintenance_hours_per_month": 1, "build_hours": 30,
"read_only": false, "reversible": true, "deletes_data": false, "max_targets": 1,
"needs_judgement": false},
{"name": "fleet-config-change", "runs_per_month": 2, "manual_minutes": 180,
"residual_minutes": 20, "maintenance_hours_per_month": 3, "build_hours": 60,
"read_only": false, "reversible": true, "deletes_data": false, "max_targets": 200,
"needs_judgement": false},
{"name": "decommission-tenant", "runs_per_month": 4, "manual_minutes": 120,
"residual_minutes": 30, "maintenance_hours_per_month": 2, "build_hours": 80,
"read_only": false, "reversible": false, "deletes_data": true, "max_targets": 1,
"needs_judgement": false},
{"name": "capacity-review", "runs_per_month": 0.33, "manual_minutes": 240,
"residual_minutes": 120, "maintenance_hours_per_month": 1, "build_hours": 40,
"read_only": true, "reversible": true, "deletes_data": false, "max_targets": 1,
"needs_judgement": true}
]
- Save the script as
score_candidates.pyand the list ascandidates.json; runpython3 score_candidates.py candidates.json. - Replace the list with candidates from your toil log, including the minutes a person still spends per automated run.
Expected output (step 1, copied from the verified run):
candidate manual_h net_h breakeven tier advice
restart-stuck-worker 10.0 7.8 5.1 mo 1 automate; supervised, then promote
decommission-tenant 8.0 4.0 20.0 mo 3 automate with approval and soft delete
tls-cert-renewal 4.5 3.0 10.0 mo 1 automate; supervised, then promote
fleet-config-change 6.0 2.3 25.7 mo 2 automate; supervised, then promote
capacity-review 1.3 -0.3 never 0 only for consistency or safety
Validation: the fleet change pays back slowly (25.7 months) yet is worth automating for consistency; the capacity review never
pays back. Troubleshooting: KeyError means a missing field; JSONDecodeError is often a trailing comma. Teardown: delete
the files, or keep the list as your first register.
Lab 2 (intermediate): run and break a guarded runner
Link to section “Lab 2 (intermediate): run and break a guarded runner”Objective: see each control work, then reproduce three real failure patterns. Prerequisites: Python 3.8+ and bash on Linux
(step 10 uses flock from util-linux); the reference runner saved as guarded_runner.py. No cost; only
the lab directory changes.
Example status: verified by execution (Python 3.8.10, bash, 2026-10-16; re-run 2026-10-17). Run ids and timings will differ.
#!/usr/bin/env bash
# Create a fake fleet of eight "hosts" as JSON files. Nothing outside ./fleet is touched.
set -euo pipefail
mkdir -p fleet
host() { # id kind tmp_files fail [hang_seconds] [healthy]
{
printf '{"id": "%s", "kind": "%s", "tmp_files": %d, ' "$1" "$2" "$3"
printf '"fail": %s, "hang": %d, "healthy": %s}\n' "$4" "${5:-0}" "${6:-true}"
} > "fleet/$1.json"
}
host app-01 app 12 false
host app-02 app 0 false
host app-03 app 7 false
host app-04 app 30 false
host app-05 app 9 true
host app-06 app 3 false 30 # hangs for 30 seconds
host app-07 app 4 false 0 false # unhealthy after a change
host db-01 db 5 false
printf 'app-01\napp-02\napp-03\napp-04\n' > targets.txt
ls fleet
- Save the script as
make_fleet.sh; runbash make_fleet.sh. - Dry run:
python3 guarded_runner.py --fleet fleet --targets targets.txt. - Add
--apply; then run it again. - Empty list (Diskerase):
: > empty.txt; run with--targets empty.txt --apply. - Wrong type (Atlassian):
printf 'app-03\ndb-01\n' > wrong.txt; run with--targets wrong.txt --apply. - Failure budget:
rm -rf fleet && bash make_fleet.sh;printf 'app-05\napp-01\napp-03\napp-04\n' > fail.txt; use--targets fail.txt --apply. - Kill switch: rebuild the fleet,
touch KILL, repeat step 3, thenrm KILL. - Time limit: rebuild;
printf 'app-06\napp-01\n' > slow.txt; run with--targets slow.txt --apply --timeout 2. - Health check: rebuild;
printf 'app-07\napp-01\napp-03\n' > sick.txt; run with--targets sick.txt --apply. - One run at a time: rebuild;
flock runner.lock sleep 5 & sleep 1; repeat step 3. - Guard tests: save the test below as
test_guards.pybeside the runner; runpython3 -m unittest test_guards.
Example status: verified by execution (Python 3.8.10, 2026-10-17).
#!/usr/bin/env python3
"""Guard tests for guarded_runner.check_targets: one case per known-bad input.
Every postmortem that finds a new bad input adds a case here.
"""
import unittest
from guarded_runner import check_targets
FLEET = {"app-01": {"kind": "app"}, "app-02": {"kind": "app"}, "db-01": {"kind": "db"}}
def problems(targets, max_targets=5):
return check_targets(targets, FLEET, "app", max_targets)
class GuardTests(unittest.TestCase):
def test_empty_list_is_refused(self): # Diskerase: empty must not mean "all"
self.assertTrue(problems([]))
def test_unknown_target(self):
self.assertIn("unknown targets: app-99", problems(["app-01", "app-99"]))
def test_wrong_kind(self): # Atlassian: real IDs of the wrong type
self.assertIn("targets are not of kind 'app': db-01", problems(["db-01"]))
def test_duplicate_target(self):
self.assertIn("duplicate targets in the list", problems(["app-01", "app-01"]))
def test_over_the_cap(self):
self.assertTrue(problems(["app-01", "app-02"], max_targets=1))
def test_valid_list_passes(self):
self.assertEqual(problems(["app-01", "app-02"]), [])
if __name__ == "__main__":
unittest.main()
Expected output (copied from the verified run):
# step 2 (dry run)
dry-run app-01 would change (tmp_files=12)
dry-run app-02 unchanged (tmp_files=0)
dry-run app-03 would change (tmp_files=7)
dry-run app-04 would change (tmp_files=30)
DONE: dry-run on 4 target(s), 0 failure(s), run c942b43f
# step 3, second run (idempotence)
apply app-01 unchanged (tmp_files 0 -> 0)
...
# step 4 (exit code 2)
REFUSED: target list is empty: nothing to do, refusing rather than guessing
# step 5 (exit code 2)
REFUSED: targets are not of kind 'app': db-01
# step 6 (exit code 3)
apply app-05 FAILED: simulated failure on app-05
apply app-01 changed (tmp_files 12 -> 0)
HALTED: 1 failure(s) exceed the budget of 0 after batch 1
# step 7 (exit code 4)
KILLED: kill switch KILL present before batch 1; 2 batch(es) not run
# step 8 (exit code 3, after about 2 seconds)
apply app-06 FAILED: time limit exceeded
apply app-01 changed (tmp_files 12 -> 0)
HALTED: 1 failure(s) exceed the budget of 0 after batch 1
# step 9 (exit code 3)
apply app-07 changed (tmp_files 4 -> 0)
apply app-01 changed (tmp_files 12 -> 0)
HALTED: health check failed after batch 1: app-07
# step 10 (exit code 2)
REFUSED: another run holds the lock runner.lock
# step 11 (exit code 0)
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
Validation: echo $? gives 0, 0, 2, 2, 3, 4, 3, 3, 2 and 0 for steps 2–11; audit.jsonl has a JSON line per target or decision.
In step 6 app-01 still changed after app-05 failed: the budget is checked between batches, the in-flight behaviour AWS also
documents.[22] In step 9 app-03 never ran. Remove any one of the five target checks in check_targets from a copy of
the runner and exactly one test fails.
Troubleshooting: FileNotFoundError means step 1 was skipped; create KILL where you run the runner. Teardown:
rm -rf fleet targets.txt empty.txt wrong.txt fail.txt slow.txt sick.txt audit.jsonl runner.lock __pycache__.
Lab 3 (advanced): port the guards to Ansible and prove idempotence
Link to section “Lab 3 (advanced): port the guards to Ansible and prove idempotence”Objective: prove each guard of the Ansible version. Prerequisites: Ansible; two to four
disposable Linux containers or VMs in group app_servers, with old files in /var/tmp/app (touch -d '10 days ago' /var/tmp/app/old.tmp),
each answering http://<host>:8080/healthz (for a lab, python3 -m http.server 8080 in a directory holding a healthz file).
Risk: it deletes files; never use real servers. Status: not executed in this environment.
ansible-playbook -i inventory.ini cleanup-tmp.yml --check --diff: the plan.findruns normally in check mode, so the removal task reports each old file as changed, and nothing is removed.[28][59]- Run without
--check; then run again. Expected: the secondPLAY RECAPshowschanged=0for every host. - Set
max_targets: 1and run: the assert fails on the first batch and no host changes. touch /etc/automation/KILL-cleanup-tmpon the control node and run: the play stops before the first batch; remove the file.- Stop the health endpoint on one host and run: the play stops after that host’s batch.
- Add ansible-lint and a Molecule scenario to CI; Molecule’s default test sequence includes the
idempotencestep.[33]
Expected output and validation: as stated in each step; the log names the guard that stopped the run.
Troubleshooting: if step 1 lists no files, check the path and file ages; a command probe in place of find would be skipped in check mode. Teardown: destroy the containers or VMs.
Use cases and scenarios
Link to section “Use cases and scenarios”The first three are illustrative; the fourth draws on Atlassian’s published review.
| Scenario | Context | Method | Trade-offs | Success measure |
|---|---|---|---|---|
| Self-service restarts | Support waits hours for ops to restart a stuck service | Tier-1 runbook job; access by role; “ensure running”; audit | More people can touch production | Wait falls to minutes; no induced incidents |
| Auto-remediation | A worker hangs several nights a week | Event rule limited to the alerting host; once_within per host; fleet cap of 3 an hour, then a person |
Can hide a real defect | Pages fall; repeats tracked as a defect |
| Fleet configuration | 200 servers need a monthly setting change | Tier 2: dry run, canary, serial 25%, failure budget 0 |
Slower than one parallel run | No run exceeds one batch of damage |
| Destructive clean-up | Removing a legacy component across customer sites | Tier 3: type-checked IDs, names shown, soft delete, approval, own sites first | Weeks slower; retention storage | No unrecoverable deletion |
Pitfalls. Self-service inputs and alert webhooks are an attack surface; remediation can itself remove capacity. Atlassian’s sites were deleted “via a standard workflow”, so monitoring saw nothing.[11]
Measure, secure and pay
Link to section “Measure, secure and pay”Metrics and KPIs
Link to section “Metrics and KPIs”No standard set exists; this is this Atlas’s proposal, with one row adapted from DORA.
| KPI | Definition and formula | Data source | Pitfall | Cadence |
|---|---|---|---|---|
| Net hours returned | runs × (manual − residual minutes) ÷ 60 − maintenance hours, per month | Run log, estimates | Maintenance left out | Monthly |
| Run success rate | successful production runs ÷ production runs | Audit log | “Success” that quietly handed off | Weekly |
| Human fallback rate | runs handed to a person ÷ production runs | Audit log, pages | Informal fallbacks never logged | Weekly |
| Induced incident rate | runs that caused an incident or needed immediate intervention ÷ production runs (adapted from DORA’s change fail rate[35]) | Incidents linked to run ids | Incidents not linked to runs | Monthly |
| Guard trip rate | runs refused or halted ÷ runs | Audit log | Zero may mean guards test nothing | Monthly |
| Time to stop | kill switch pulled → last in-flight action finished | Drill records | Unmeasured until the real incident | Each drill |
| Ownership coverage | automations with a current owner and review date ÷ registered | Register | Owners that no longer exist | Quarterly |
| Unregistered automation | jobs or keys acting on production but not registered | Credential and scheduler inventory | Counting only what you know | Quarterly |
Toil share is defined in SRE.
Leading and lagging: fallback rate, guard trips and ownership coverage lead; induced incidents lag. Vanity traps: counting automations; “hours saved” without maintenance. Minutes are estimates: revisit them yearly.
Security, safety and compliance
Link to section “Security, safety and compliance”- Identity is the blast radius. One least-privilege identity per automation; NIST’s AC-6 covers “processes acting on behalf of users”.[34] Check whose rights a run uses (trust boundaries) and harden service-role trust policies against the “confused deputy problem”.[60]
- Inputs are an attack surface. Authenticate every trigger, webhooks included;[56] constrain parameters with patterns,
and for SSM command documents prefer
interpolationType: ENV_VARto string substitution.[54] Secrets come from a secrets manager at run time. - Approvals need separation and scope. Requester and approver differ (“two-person control”[34]); whoever can assume an approver role can approve;[41] the approval must cover the action taken.[17]
| NIST SP 800-53 control[34] | What it asks | AutomationOps practice |
|---|---|---|
| CM-3 | Decide which changes are controlled; review, approve, document, retain records | Register, tiers, audit log retention |
| CM-3(2) | Test, validate and document changes before finalising them | Lint, idempotence and guard tests; dry runs |
| CM-3(3) | Implement changes using automated mechanisms | The executor itself, under the controls above |
| CM-5 | Restrict who can make changes | Who may edit, trigger and approve each automation |
| CM-5(4) | Dual authorization for changes to system components | Two people approve changes to automation code and configuration |
| AC-3(2) | Dual authorization for privileged commands | Second-person approval for tier-3 runs |
| AC-6 | Least privilege, including processes | One identity per automation |
These controls bind only where adopted. Mapping is covered in ComplianceOps, change approval in ITOps.
Cost, capacity and sustainability
Link to section “Cost, capacity and sustainability”Maintenance is the lasting cost, because automation must change with the systems under it (“bit rot”[1]). Add the runner platform (fees change: AWS ended the Systems Manager Automation free tier for new customers on 14 August 2025 and for existing ones on 31 December 2025[43]), identities and on-call time. Capacity: “even read operations, naively implemented, can spike device load”, and at scale “these safety checks can eventually dominate workload”.[15] Unit view: cost per automated run (platform share plus residual minutes) against cost per manual run. Sustainability is rarely material (FinOps covers idle resources).
Assurance of the operating model
Link to section “Assurance of the operating model”| Check | How | Signal of trouble |
|---|---|---|
| Kill-switch drill | Pull the switch during a staged run; time it | Time to stop unknown or rising |
| Guard tests | CI cases for empty, unknown, wrong-type and oversized inputs | A guard change without a failing test |
| Audit sampling | Monthly: did targets match the request, and did someone else approve? | Self-approvals; unexplained targets |
| Dead-man alerts | Alert when a scheduled automation has not run | Silent stops after credential expiry |
| Register reconciliation | Compare the register with credential use and schedulers | Keys and cron jobs nobody can explain |
| Manual-path practice | Rehearse the manual procedure for critical automations | Nobody on call has done it by hand |
The last row follows Bainbridge: “physical skills deteriorate when they are not used”.[13] Facebook credited its “storm” drills with its careful 2021 restart.[10]
When things go wrong
Link to section “When things go wrong”Common failures and troubleshooting
Link to section “Common failures and troubleshooting”| Symptom | Likely cause | Diagnosis | Remediation | Prevention |
|---|---|---|---|---|
| Far more targets changed than intended | Empty selector, mistyped input, wrong ID type[1][9][11] | Run’s target count and types vs the request | Kill switch; restore | Validation, caps, floors, names |
| Remediation keeps firing; capacity drains | No velocity limit; flapping checks[12] | Actions per target per hour | Disable the automatic action | Throttles, velocity caps, demotion |
| Two instances undo each other | Concurrent executors; check made long before the write[12] | Interleaved run ids on one object | Disable both; repair by hand | Locks or version checks at write time |
| A guarded command did damage | Bug in the guard[10] | Replay the input against the guard | Fix the guard first | Known-bad input tests |
| “Succeeded” but nothing changed | Check mode left on; changed_when: false |
State before vs after | Re-run correctly | Verify outcomes, not exit codes |
| Scheduled job silently stopped | Expired credential; owner left | Last successful run | Re-own; rotate | Dead-man alerts; reviews |
| Rollback made it worse | Untested rollback of partial state; Knight’s uninstall spread the fault[8] | What actually changed | Assess before reversing | Tested rollbacks |
| Nobody can do it by hand | Manual path decayed[1][13] | Drill fails | Pair with the owner | Manual-path practice |
Worked runbook: automation is changing production unexpectedly
Link to section “Worked runbook: automation is changing production unexpectedly”Diagram source (Mermaid)
flowchart TB
A["Alert or report: unexpected changes"] --> B["Pull the kill switch; disable triggers"]
B --> C{"All runs stopped?"}
C -->|"no"| D["Revoke or deny the automation's identity"]
D --> C
C -->|"yes"| E["Scope damage from the audit log"]
E --> F["Restore: soft delete, backup or roll forward"]
F --> G["Re-enable as supervised, after fix and guard test"]
G --> H["Postmortem; demote tier; update register"]
classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
class A,B,D risk
class C decision
class E,F,G,H core
Text equivalent: pull the kill switch and disable triggers; if runs continue, revoke or deny the automation’s identity until all have stopped. Then scope the damage from the audit log, restore (soft delete, backup or roll forward), re-enable only as supervised after a fix and a guard test, and run a postmortem that demotes the tier and updates the register.
- Stop (on-call, minutes): kill switch, then schedules and event rules; if anything still runs, deny its identity.
- Confirm: no new audit records for the run id.
- Scope (owner, on-call): every target touched, with before/after values; a log that cannot say is the first action item.
- Restore (service owner): soft delete, backup or a reviewed roll-forward; never reverse a partial change blindly.
- Re-enable (owner, approved by the service owner): supervised, after a test that reproduces the failing input.
- Learn: blameless postmortem (IncidentOps). Atlassian: “people don’t cause incidents. Rather, systems allow for mistakes to be made.”[11]
Anti-patterns and myths
Link to section “Anti-patterns and myths”- The script in a home directory (Google’s second level[1]): one owner, no tests, no audit. Register it and use a runner.
- Automating the mess. “You rarely want to literally transcribe a human workflow into a machine workflow.”[15]
- One god credential shared by all automation: any bug has full reach.
- Approval for everything. Approvers stop reading; keep approvals for tier 3 (editorial judgement).
- Auto-remediation as a hiding place. Nightly restarts of a leaking service silence the alert and keep the defect.
- Myth: once automated, done. Automation rots as systems change and is most fragile when rarely run.[1]
Deciding and adopting
Link to section “Deciding and adopting”When to use and when not to use
Link to section “When to use and when not to use”Use it when operational actions repeat, are understood and touch production, especially when several people or systems trigger them or an auditor will ask who changed what.
Do not automate (yet) when the task is rare and needs judgement (give people a checklist); when the need can be removed (fix the leak; build failover into the system[1]); when the process is not understood or agreed; when nobody will own the result; or when the triggering signal is unreliable (fix the signal first).
Hidden responsibilities: maintenance, an identity to secure, an on-call path for its failures, drills and eventual retirement.
Decision framework
Link to section “Decision framework”Diagram source (Mermaid)
flowchart TB Q1["1 Repeats, understood?"] -->|"no"| M["Manual checklist"] Q1 -->|"yes"| Q0["2 Need removable?"] Q0 -->|"yes"| FIX["Fix at source"] Q0 -->|"no"| Q2["3 Judgement each time?"] Q2 -->|"yes"| HB["Human-backed interface"] Q2 -->|"no"| Q3["4 Pays back, or must be consistent?"] Q3 -->|"no"| NO["Not now"] Q3 -->|"yes"| AUTO["Automate; tier sets controls"] M ~~~ FIX FIX ~~~ HB HB ~~~ NO classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029 classDef core fill:#e8f3f2,stroke:#16756f,color:#122029 classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029 class Q1,Q0,Q2,Q3 decision class FIX,AUTO core class M,HB,NO neutral
Text equivalent: four questions in order (orange boxes), each with one exit. 1, does the task repeat and is it understood? If not, keep a manual checklist. 2, can the need be removed? If so, fix it at the source. 3, does it need judgement each time? If so, build a human-backed interface. 4, does it pay back, or must it be consistent? If not, not now. Otherwise automate, with the controls its risk tier requires.
The tree assumes that removing work beats automating it. How much ceremony follows is decided by the tiers: irreversibility, not frequency, sets the controls.
Maturity model
Link to section “Maturity model”This Atlas’s model, not an industry standard; it measures governance, not the amount of automation.
| Level | Observable behaviour | Evidence | Exit criteria |
|---|---|---|---|
| 1 Ad hoc | Scripts in home directories; shared admin credentials | Shell histories | Inventory of everything that changes production |
| 2 Shared | Automation in version control and a runner | Repository, runner logs | Every production-changing automation registered with an owner |
| 3 Guarded | Dry runs, validation, caps, batches, kill switches for tiers 2–3; own identities | Guard tests, drill records | Monthly metrics; tier policy enforced by the runner |
| 4 Governed | Quarterly reviews; tiers drive approvals; fallback and induced-incident rates tracked; retirements happen | Register history, metrics | Frequent remediations moved into systems; automatic demotion |
| 5 Self-correcting | Frequent actions built into systems; velocity limits on event automation; agents use the same guards | Fewer external scripts; drills pass | Sustain; keep manual skills |
The typical stall is level 2: scripts in Git, no owners, one shared credential. The register is the way out.
Adoption roadmap: 30/60/90 days
Link to section “Adoption roadmap: 30/60/90 days”| Phase | Actions | Milestone and metric |
|---|---|---|
| Days 1–30 (platform lead, service owners) | Inventory schedulers, CI jobs, runbook tools and script keys; register the 20 riskiest; guard the most dangerous script; score candidates | Register exists; ownership coverage measured |
| Days 31–60 (platform team, owners) | One runner; own identities for the top 10; dry runs, batches and failure budgets for tier 2; first kill-switch drill | Time to stop measured |
| Days 61–90 (service owners, platform team) | Promote first automations with evidence; dead-man alerts; first quarterly review; retire unused automations; publish the tier policy | Ownership coverage ≥ 90%; fallback and induced-incident rates reported |
Quick wins: the empty-list guard; a kill switch for the automation that can delete data; a dead-man alert on certificate renewal.
Toolkit
Link to section “Toolkit”Checklists
Link to section “Checklists”Quick start
- Every automation that changes production is registered, with owner and tier.
- No automation uses a shared admin credential.
- The riskiest automation refuses empty and oversized target lists.
- Tier 2 and 3 automations have a kill switch that on-call can find.
Readiness review (preflight)
- Candidate scored; owner, service owner and tier agreed.
- Idempotence shown by a second run with no changes.
- Guard tests pass for empty, unknown, wrong-type, duplicate and oversized inputs.
- Dry run lists targets with before/after values.
- Time limits set; for tier 2, also a lock, batches, a failure budget and health checks between batches.
- Identity scoped; secrets from a manager.
- Manual fallback works; kill switch tested and timed.
Before a tier-3 run
- The plan shows names and the count matches the request.
- Soft delete or a tested restore exists.
- Approver differs from requester; on-call is told.
- The first stage targets your own resources.
Quarterly governance audit
- Owners and review dates current; frozen automations re-owned or retired.
- Register reconciled with credentials and schedulers.
- Audit sample: targets matched requests; approvals separated.
- Rising fallback rates have work scheduled; drills done.
Continuous improvement
- Every frequently firing remediation has a defect ticket; every automation postmortem adds a guard test.
Templates
Link to section “Templates”Example status: templates; nothing to execute.
AUTOMATION READINESS REVIEW
Automation id / version: Owner: Service owner:
Purpose (one sentence):
Tier (0-3) and why:
Triggers (schedule / event / person): Who may trigger:
Identity and permissions (exact actions, resources):
Target selection: source of the list; validation rules; cap; floor:
Dry run evidence (link): Idempotence evidence (link):
Time limit / lock / batches / failure budget / health check between batches:
Approval (tier 3): approver group; requester excluded? yes / no
Kill switch: location; tested on (date); time to stop:
Fallback: who is paged; manual procedure (link); last practised (date):
Metrics: success, fallback, induced incidents; dashboard (link):
Review date: Decision: approve / approve supervised only / reject
Reviewers and date:
KILL-SWITCH DRILL RECORD
Date / automation / environment:
Scenario (e.g. bad input reached batch 2):
Switch pulled by / at (time): Last in-flight action finished at (time):
Time to stop: Targets changed after the switch:
Did on-call find the switch without help? yes / no
Problems found and actions (owner, due date):
AUTOMATION RETIREMENT NOTICE
Automation id: Owner: Date:
Reason (cause fixed at source / unused for N days / replaced by):
Triggers removed: yes / no Identity and secrets revoked: yes / no
Code archived at: Register status set to retired: yes / no
Dependent runbooks and dashboards updated: yes / no
Deep comparisons
Link to section “Deep comparisons”| Approach | Best for | Main risk | Controls to add |
|---|---|---|---|
| Manual checklist | Rare, judgement-heavy work | Drift | Second-person check on risky steps |
| Personal script | Prototyping | One owner, no audit | Register and runner |
| Runbook platform or configuration management | Self-service jobs; fleet desired state | Privileged platform; fleet-wide bad change | Tiers, identities, check mode, batches |
| Event-driven remediation | Frequent, understood faults | Storms; hidden defects | Host limits, throttles, fleet caps |
| Built into the system | Very frequent actions | Hard to retrofit; less visible | Expose internals to operators[1] |
| AI operations agent | Investigation, proposals | Plausible wrong actions | Review mode, narrow identity, same guards |
Is AutomationOps a job title or a certification? We found no certification or standards body using the term for IT operations (searched 2026-10-16). SREs, platform and operations engineers do the work, sometimes in an automation centre of excellence.
Isn’t this just SRE? SRE defines and caps toil (SRE); this page makes the automation that removes toil safe and owned. Small teams do both.
Should every automation need an approval? No. Approvals add information only for irreversible or wide-reaching actions; elsewhere they train people to click. Use the tiers.
Our operational jobs run from CI pipelines. In scope? Yes, if they change production. Scope the pipeline’s credentials to the job; pipeline identity is covered in DevSecOps; infrastructure-as-code runs follow InfraOps.
How much autonomy should auto-remediation have? As much as its tier and record allow, always with a velocity limit; see the judgement call.
Going further
Link to section “Going further”Advanced topics and emerging direction
Link to section “Advanced topics and emerging direction”- Fighting control loops. In AWS’s DynamoDB DNS system a one-time freshness check “was stale by this time”, so an older plan overwrote a newer one.[12] Prefer one writer, versioned plans and compare-and-set at write time; durable engines are in WorkflowOps.
- Global change budgets and cells. Cap how much capacity all automation may remove at once (AWS’s announced NLB velocity control caps one mechanism[12]); automation that acts one cell at a time inherits the cell’s limit, which is why AWS refactored parts of S3 “into smaller cells to reduce blast radius and improve recovery”.[9]
- Verification before execution is mature in networking (NetworkOps); elsewhere, dry runs and guards dominate.
- AI agents (emerging): approval modes and permission scopes exist;[17][16] we found no independent evidence on their production error rates.
Learning path
Link to section “Learning path”| Stage | What to do | Outcome |
|---|---|---|
| Prerequisites | Shell and Python basics; the SRE book’s toil chapter[18] | Can read and write small scripts |
| 1-day primer | First four parts of this page; the S3, Facebook and Atlassian reports;[9][10][11] Lab 1 | Can score candidates and name the five controls |
| 1-week practice | Lab 2; add a floor guard (refuse if fewer than two hosts would stay healthy) with a test | Can build and test guards |
| 30-day progression | Lab 3 with Molecule in CI; register your team’s automations; a kill-switch drill | A governed automation in production |
| Specialisation | Event-driven remediation; runbook platforms; access design with IAMOps | Can design a platform’s controls |
| Portfolio | A guarded runner with tests; an analysis of a public automation incident | Evidence of judgement |
Wrap-up
Link to section “Wrap-up”Executive recap and cheat sheet
Link to section “Executive recap and cheat sheet”| Topic | Remember |
|---|---|
| What it is | Governance and safety of operational automation; an editorial reading of an ambiguous label |
| Decide | Remove the need first; automate repetitive, understood work; irreversibility decides the ceremony |
| Five controls | Dry run · target validation (empty never means all) · blast-radius cap with batches · failure budget · kill switch |
| Ownership | Register, owner, tier, own identity, review date, retirement |
| Metrics | Net hours returned, fallback rate, induced-incident rate, time to stop, ownership coverage |
| Watch-outs | Shared credentials, approval fatigue, remediation storms, open webhooks, dry runs as proof, lost manual skills |
| Next actions | Register; guard the riskiest script; drill a kill switch; score three candidates |
A judgement call: automatic failover or a human decision?
Link to section “A judgement call: automatic failover or a human decision?”Situation (illustrative). A checkout database has a primary and a replica, usually under a second apart. Automated failover takes about 30 seconds; a human-approved one at night about 15 minutes. Last year the health check raised 12 alarms, 2 of them real primary failures.
| Option | Real failures (2 a year) | False alarms (10 a year) | Total |
|---|---|---|---|
| A: automatic failover | 2 × 0.5 min = 1 min | 10 × 0.5 min = 5 min of avoidable disruption, each risking writes inside the lag | about 6 min; 12 risky failovers |
| B: automatic diagnosis, human approves | 2 × 15 min = 30 min | 0 min, but 10 night pages | about 30 min; 12 pages |
The attractive choice and why it can be wrong. A wins on minutes, but failover can lose acknowledged writes (tier 3), a flapping check can trigger repeated failovers, and the signal is wrong 10 times in 12. B costs half an hour of outage a year and night pages for alarms that are usually false.
Recommendation. Fix the signal first. Then choose A with conditions: fail over automatically only if replication lag, checked at the moment of action, is within the agreed data-loss limit, the old primary can be fenced, and no automatic failover happened in the last few hours; otherwise fall back to B. Uncertainty: the figures are illustrative; engine mechanics are in DBOps, invoking DR in DisasterRecoveryOps.
Learning objectives and mastery check
Link to section “Learning objectives and mastery check”After this page you should be able to explain the label and this page’s reading of it; score candidates; design guards, batches, failure budgets and kill switches; govern with a register, identities and metrics; and evaluate when not to automate. Self-check:
- A decommission script takes its server list from an inventory query, which returns nothing. What should the script do?
Show answer for question 1
Refuse and report: empty must never mean “everything” (the Diskerase incident).[1] Page the owner if the empty result is unexpected.
- A colleague says a playbook is safe because
--checkshowed no errors. What is missing?
Show answer for question 2
Check mode is “just a simulation”: unsupported modules “report nothing and do nothing”, and conditions on registered results show
nothing when the task that registered them was skipped, as a command probe is.[20][27] Add --diff, compare the
target count with expectations, and rely on guards and batches as well.
- An SSM Automation run targets 50 instances with
--max-concurrency 10and--max-errors 10%. How many could fail?
Show answer for question 3
- A task runs 10 times a month, takes 30 minutes by hand, would need 5 minutes of human time per automated run and 2 hours of maintenance a month, and 50 hours to build. Automate it?
Show answer for question 4
Net = 10 × (30 − 5) ÷ 60 − 2 = 2.17 hours a month; break-even about 23 months. Marginal on time alone: automate if consistency or speed matters or maintenance can shrink; otherwise improve the checklist and revisit.
- A nightly job deletes customer records past retention across all tenants. Approving every run would be tedious. How do you govern it?
Show answer for question 5
Tier 3, but put the second person on changes to the job’s code and configuration, not on every run. Each run needs automatic guards: soft delete with a recovery window, a cap, and an anomaly check that refuses if far more records qualify than yesterday, in line with Atlassian’s soft-delete and staged-rollout lessons.[11]
Conclusion
Link to section “Conclusion”AutomationOps is a borrowed name: RPA vendors use it, and this Atlas applies it to something older, keeping operational automation safe and owned. In each incident above, automation did what it was told; it was told something wrong (an empty list, a mistyped input, the wrong IDs, a stale plan) and nothing stopped it in time.
The discipline is simple to state: remove what need not exist, automate what repeats and is understood, give every run the charter’s brakes, including a kill switch someone has actually pulled, and give each automation an owner, an identity and a retirement date. Keep people able to do the work by hand: the day the automation fails is the day they will need to.
Sources and further reading
Link to section “Sources and further reading”Sources were opened on the date each footnote gives; dates are the source’s own. Claim-level support is in
research/automationops-ledger.md. Re-check by 2027-04-17, then every six months: agent modes and defaults, Systems Manager
pricing, Rundeck and StackStorm releases; open the SEC and Red Hat pages in a browser (they block some automated clients).
Changelog. 2026-10-16, r0: first draft; runner and scoring labs executed; other examples syntax-checked only.
2026-10-17, r1 (answers reviews/automationops-r1.md): safer Event-Driven Ansible and AWS examples, corrected check-mode advice,
runner with lock, time limits and health checks (Lab 2 re-run), two diagrams redrawn, dates, paraphrases and NIST mapping fixed;
14 sources added, all checked 2026-10-16.
2026-10-17, post-score polish: IAM policy caveats (2 sources added); tag-targeting inference labelled; Autonomous → Retired
edge; Lab 1 and register fit the column; executed guard tests in Lab 2; sentences split; re-check cadence.
The Evolution of Automation at Google — Niall Murphy with John Looney and Michael Kacirek, Site Reliability Engineering (Google, O’Reilly, 2016). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
AutomationOps: A Modern Approach to Automation (archived) — Robocorp, snapshot 2023-09-23; the live URL now redirects to sema4.ai. Checked 2026-10-16. ↩︎ ↩︎
Automation Ops release notes: General Availability — UiPath documentation, release date 2021-04-26. Checked 2026-10-16. ↩︎ ↩︎
Automation Ops user guide: Introduction — UiPath documentation, latest. Checked 2026-10-16. ↩︎
What is IT automation? — Red Hat, 2024-11-20. Checked 2026-10-16. ↩︎
Rundeck Introduction — Rundeck documentation (PagerDuty); latest open-source release v6.2.1, 2026-09-09. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
AutoOps — Elastic, undated product page. Checked 2026-10-16. ↩︎
In the Matter of Knight Capital Americas LLC, Release No. 34-70694 — U.S. Securities and Exchange Commission, 2013-10-16 (blocks non-browser clients). Checked 2026-10-16. ↩︎ ↩︎
Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region — Amazon Web Services, 2017 (event of 2017-02-28). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
More details about the October 4 outage — Santosh Janardhan, Engineering at Meta, 2021-10-05. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Post-Incident Review on the Atlassian April 2022 outage — Atlassian, 2022-04-29. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Summary of the Amazon DynamoDB Service Disruption in the Northern Virginia (US-EAST-1) Region — Amazon Web Services, October 2025. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Ironies of Automation (author’s web copy, archived) — Lisanne Bainbridge; originally Automatica 19(6), 1983; snapshot 2016-03-05. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Is It Worth the Time? — Randall Munroe, xkcd 1205, 2013-04-29. Checked 2026-10-16. ↩︎ ↩︎
Eliminating Toil — David Challoner, Joanna Wijntjes, David Huska et al., The Site Reliability Workbook (Google, O’Reilly, 2018). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Announcing General Availability of AWS DevOps Agent — Madhu Balaji, AWS Cloud Operations Blog, 2026-03-31. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Run modes in Azure SRE Agent — Microsoft Learn, updated 2026-06-11. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Eliminating Toil — Vivek Rau, Site Reliability Engineering (Google, O’Reilly, 2016). Checked 2026-10-16. ↩︎ ↩︎
Glossary — Ansible Community Documentation. Checked 2026-10-16. ↩︎
Validating tasks: check mode and diff mode — Ansible Community Documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Controlling playbook execution: strategies and more — Ansible Community Documentation. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Control automations at scale — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Feature Toggles (aka Feature Flags) — Pete Hodgson, martinfowler.com, 2017-10-09. Checked 2026-10-16. ↩︎
Making retries safe with idempotent APIs — Malcolm Featonby, Amazon Builders’ Library, undated. Checked 2026-10-16. ↩︎ ↩︎
Operational excellence design principles — AWS Well-Architected Framework. Checked 2026-10-16. ↩︎ ↩︎
Setting up Automation — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎
ansible.builtin.command module — Ansible Community Documentation. Checked 2026-10-16. ↩︎ ↩︎
ansible.builtin.find module — Ansible Community Documentation. Checked 2026-10-16. ↩︎ ↩︎
Error handling in playbooks — Ansible Community Documentation. Checked 2026-10-16. ↩︎ ↩︎
start-automation-execution — AWS CLI 2.38.1 Command Reference. Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
ShellCheck, a static analysis tool for shell scripts — koalaman/shellcheck, GitHub. Checked 2026-10-16. ↩︎ ↩︎
no-changed-when — Ansible Lint documentation. Checked 2026-10-16. ↩︎ ↩︎
Molecule configuration — Ansible Molecule documentation (Red Hat). Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Security and Privacy Controls for Information Systems and Organizations (SP 800-53 Rev. 5) — NIST, 2020-09 (updates as of 2020-12-10). Checked 2026-10-16. ↩︎ ↩︎ ↩︎ ↩︎
DORA’s software delivery performance metrics — DORA. Checked 2026-10-16. ↩︎ ↩︎
Our Plans for Open Source Puppet in 2025 — Puppet (Perforce), 2024-11-07. Checked 2026-10-16. ↩︎
OpenVox: The Community-Driven Fork of Puppet Has Arrived — Steven J. Vaughan-Nichols, The New Stack, 2025-01-23. Checked 2026-10-16. ↩︎
StackStorm Overview — StackStorm documentation; latest GitHub release 3.9.0, 2025-10-10. Checked 2026-10-16. ↩︎
Conditions: throttle actions to counter event storms — ansible-rulebook documentation, latest. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Limiting hosts — ansible-rulebook documentation, latest. Checked 2026-10-16. ↩︎ ↩︎
aws:approve – Pause an automation for manual approval — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎ ↩︎ ↩︎
Azure Automation overview — Microsoft Learn. Checked 2026-10-16. ↩︎
AWS Systems Manager Automation — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎ ↩︎
Special variables — Ansible Community Documentation. Checked 2026-10-16. ↩︎
Playbook Keywords — Ansible Community Documentation. Checked 2026-10-16. ↩︎
AWS-RestartEC2Instance — AWS Systems Manager Automation Runbook Reference. Checked 2026-10-17. ↩︎
Run an automation step by step — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎
How EC2 instance stop and start works — Amazon EC2 User Guide. Checked 2026-10-16. ↩︎
Mapping targets for an automation — AWS Systems Manager User Guide. Checked 2026-10-17. ↩︎
Example policies to control access the Amazon EC2 API — Amazon EC2 User Guide. Checked 2026-10-16. ↩︎
Requirements for Amazon EBS encryption — Amazon EBS User Guide. Checked 2026-10-17. ↩︎
aws:changeInstanceState – Change or assert instance state — AWS Systems Manager User Guide. Checked 2026-10-17. ↩︎
Grant a user permissions to pass a role to an AWS service — AWS Identity and Access Management User Guide. Checked 2026-10-16. ↩︎
Data elements and parameters — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎ ↩︎
Rulebooks — ansible-rulebook documentation, latest. Checked 2026-10-16. ↩︎
Event Sources — ansible-rulebook documentation, latest. Checked 2026-10-16. ↩︎ ↩︎
Configure rulebooks to take action in response to events or conditions — Red Hat Ansible Automation Platform 2.6 documentation. Checked 2026-10-16. ↩︎ ↩︎
Rules — ansible-rulebook documentation, latest. Checked 2026-10-16. ↩︎
ansible.builtin.file module — Ansible Community Documentation. Checked 2026-10-16. ↩︎
Create the service roles for Automation using the console — AWS Systems Manager User Guide. Checked 2026-10-16. ↩︎