AnsibleVII · Ad-Hoc ExecutionAd-hoc execution
Blast radius of a one-line command
What you'll learn
- Explain why an ad-hoc command carries more risk than an equivalent playbook
- Apply the list-hosts, check, limit habit before any ad-hoc command that writes
- Recognise the pattern mistakes that widen scope silently rather than failing
- Predict what --check does and does not evaluate in an ad-hoc run
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
A playbook that reconfigures 300 servers is a file. It was written, saved, probably reviewed, and run by someone who had to type a filename and think about which inventory to point at it.
An ad-hoc command that reconfigures 300 servers is 60 characters typed into a terminal by someone who had the idea eleven seconds ago.
The playbook is bigger, more complex and does more. The ad-hoc command is more dangerous. There is no step between thinking it and executing it, and every safeguard Ansible offers is something you must remember to add.
The specific risk is the pattern
Everything else in an ad-hoc command is visible. You can see the module.
You can see the arguments. You can see -b.
What you cannot see is what the pattern resolves to, because that depends
on an inventory file you are not looking at, possibly assembled from
several sources, possibly with children: relationships someone added
last month.
The glob case is worth seeing, because it is the one that catches experienced people:
$ ansible -i inventory.ini '*01*' --list-hosts hosts (3):
web01.example.com
db01.example.com
cache01.example.comHost patterns have their own part later in this course, which covers the
full syntax — unions, intersections, exclusions, regex patterns and
--limit semantics — properly. What you need here is narrower and
non-negotiable: you do not know what a pattern matches until you have
asked.
The three-step habit
Before any ad-hoc command that is not read-only:
Step 1 — --list-hosts, and read the count
$ ansible production -i inventory.ini --list-hosts hosts (5):
web01.example.com
web02.example.com
web03.example.com
db01.example.com
db02.example.comThe count is the number that matters. You had a number in your head before you ran this — five, or forty, or “just the web tier”. If the printed count differs from the one in your head, stop and find out why before doing anything else. That mismatch is the single highest-value signal available in this entire workflow, and it is free.
Step 2 — --check, where the module supports it
$ ansible production -i inventory.ini -b -m ansible.builtin.lineinfile -a "path=/etc/ssh/sshd_config regexp='^PermitRootLogin' line='PermitRootLogin no'" --check --diffNote the qualifier: where the module supports it. Check mode is a per-module property, not a global guarantee, and the modules you are most likely to reach for in an ad-hoc command are the worst at it.
$ ansible localhost -m ansible.builtin.command -a "/usr/local/bin/migrate.sh" --checklocalhost | SKIPPEDSKIPPED is not “no changes needed”. It is “this task was not evaluated
at all”. A --check run of a command task tells you precisely nothing,
and if you read that output as reassurance you are worse off than if you
had not run it — you now have false confidence. The next part gives this
its own lesson, because it is the most misunderstood behaviour in
Ansible.
Step 3 — --limit, always
$ ansible production -i inventory.ini --limit web01.example.com -b -m ansible.builtin.service -a "name=nginx state=restarted"--limit narrows an already-resolved set. It is not a second pattern for
selecting hosts; it is a bound on the first one. That difference has a
useful consequence: a --limit that matches nothing is an error, not
a no-op.
$ ansible production -i inventory.ini --limit webs --list-hosts; echo "exit=$?"[WARNING]: Could not match supplied host pattern, ignoring: webs
[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.
exit=1That is the behaviour you want. A mistyped bound stops the run rather than silently letting the unbounded pattern through.
The forks multiplier
-f decides how many hosts are worked on simultaneously. The default of
5 is the last remaining accidental safeguard in the ad-hoc workflow.
With -f 5 against 200 hosts, a destructive mistake damages five, then
five more, and you have perhaps thirty seconds of visibly wrong output
scrolling past before your hand reaches Ctrl-C. Roughly 15 to 25 hosts
are lost. That is an incident.
With -f 200, all 200 are damaged in the first batch. Ctrl-C achieves
nothing because there is nothing left to stop. That is an outage.
Same command, same mistake, two different Mondays. When you raise forks for a legitimate performance reason — and there are legitimate reasons — you are also removing the reaction window, and you should know that is what you are doing.
Why the playbook is safer despite being bigger
Line for line, a playbook doing the same work has more places to go wrong. It is still the safer artefact, for reasons that have nothing to do with the YAML:
- The
hosts:line is written down. It can be read, reviewed and diffed. A pattern typed at a prompt is reviewed by nobody. - It is run twice. Once in
--checkagainst staging, once for real. Nobody runs an ad-hoc command twice on purpose. - It can express dependencies. “Restart only where the config actually changed” is a handler. Ad-hoc cannot say it.
- It can carry its own preconditions. An
assertthat fails closed if the host is not what you expected has nowhere to live in a one-liner. - It leaves a
PLAY RECAP. Per-host counts of ok, changed, failed and unreachable, in one place. Ad-hoc output has no recap at all.
The asymmetry is the point of this part: the tool with fewer safeguards is the one people reach for when they have least time to think.
Knowledge check
Knowledge check · 4 questions
Q1. Which of these mistakes will Ansible NOT warn you about?
Q2. You run `ansible production --check -m ansible.builtin.command -a "/usr/local/bin/migrate.sh"` and every host reports SKIPPED. What have you learned? Select all that apply.
Q3. Pressing Ctrl-C during a destructive ad-hoc run leaves the hosts in the currently executing batch in an unknown state, because the module is already running on the target and its result is discarded.
Q4. You have time for exactly one of the three safety steps before an urgent ad-hoc change. Which gives the most protection, and why?
Passing score: 75%. Answers are checked in this browser.