Chapter 3: Modules & Ad-hoc Commands
Topic 13

Ad-hoc Commands in Depth

WorkflowAd-hoc

An ad-hoc command runs exactly one module against a host pattern in a single shot, no playbook involved: ansible web -m service -a "name=nginx state=restarted" --become. It picks the hosts, the module, and the module's arguments straight from the command line and reports per host, which makes it the fastest way to touch the fleet for a one-off.

It is the right tool for a single operation across many machines — restart a service, install a package once, read a fact, check uptime everywhere — and the wrong tool for anything you will run twice. Nothing about an ad-hoc command is version-controlled, reviewed, or repeatable; it lives in your shell history and nowhere else.

The Anatomy of a Run

The shape is ansible <pattern> -m <module> -a "<args>": the pattern selects hosts, -m names the module, and -a carries its arguments. The pattern resolves through the same inventory a playbook uses, so ansible web hits web1.larkspur.io and web2.larkspur.io together and prints a result block per host.

That shared resolution is the point. The groups you built in Chapter 2 — web, db, all — mean the same thing here as in a play, so an ad-hoc command is a play's single task lifted onto the command line. Learn the pattern syntax once and it serves both.

One ad-hoc command — one module, one shot
ansible <pattern> -m <module> -a "<args>" [--become]
One module, one shotruns against every matched host
Result per hostok / changed / failed

The Argument String

Module arguments are key=value pairs inside one quoted string: -a "name=nginx state=restarted". These are the exact arguments the module takes in a playbook task, flattened onto the command line, which is why running a module ad-hoc is the quickest way to learn its options without writing any YAML.

The quoting carries weight. The whole string is one argument to ansible, so the shell must not eat the spaces or the equals signs — wrap it in double quotes and let the module parse the pairs. A mis-quoted string reaches the module garbled and fails with a parse error that looks nothing like the real problem.

Privilege with --become

Most real operations need root: installing a package, restarting a system service, editing a file under /etc. The --become flag runs the module under sudo on the Ubuntu nodes, escalating after the SSH login. Without it the command connects as your unprivileged login user and the operation is refused.

Forgetting --become is the difference between a clean run and a permission error that reads like a broken module. The apt module is not failing — it simply cannot write to the package database as a normal user. The fix is the flag, not the module.

Fleet Diagnosis

The read-only half of why ad-hoc exists is answering "what does the fleet look like right now?" across every host at once. ansible all -m command -a "uptime" reports load on all of them, ansible db -m setup -a "filter=ansible_mounts" shows the database hosts' mounts, and ansible web -m service_facts lists which units are running on the web tier.

These gather information and change nothing, so they are safe to fire at all without a second thought. Using -m setup and -m service_facts to explore a host's facts before you reference them in a template or conditional beats guessing at fact names from memory.

One-off Convergence and the Firm Line

Ad-hoc can also change state once: ansible web -m apt -a "name=htop state=present" --become installs a package idempotently without writing a play. That is legitimate for break-glass and exploration — and it is exactly the thing a role exists to replace the moment the change becomes permanent.

The firm line is this: ad-hoc commands leave no record, get no review, and re-running a remembered chain of them by hand is how a server becomes a snowflake again. The moment a change belongs in the configuration, it belongs in a playbook, not your shell history. Keep ad-hoc for diagnosis and the genuinely one-time fix.

Common Mistakes
  • Running ansible all -m service -a "name=postgresql state=restarted" --become to "restart one thing" and bouncing PostgreSQL on db1.larkspur.io along with every other service-bearing host, because all was the pattern and the blast radius was never checked.
  • Forgetting --become on ansible web -m apt -a "name=nginx state=present" and reading the permission error that follows as a broken apt module instead of a missing privilege escalation.
  • Building a server's configuration out of a remembered sequence of ad-hoc commands, so there is no record of what was done and the next person rebuilds web2 by guesswork.
  • Quoting the -a string wrong so the shell eats the spaces or the equals signs, and the module receives garbled arguments and fails with a confusing parse error rather than the intended action.
  • Reaching for -m shell -a "systemctl restart nginx" when -m service -a "name=nginx state=restarted" exists, throwing away idempotency and honest change reporting for a one-off that did not need the shell.
Best Practices
  • Scope the pattern tightly — web1.larkspur.io, not all — for any ad-hoc command that changes state, since it runs immediately with no plan to review and no undo.
  • Keep ad-hoc for diagnosis and break-glass: gather a fact, restart a stuck service, check uptime. Promote anything you would run twice into a playbook.
  • Pass --become whenever the operation touches root-owned resources, and reach for the real module (apt, service, copy) over command/shell even in a one-off.
  • Use -m setup and -m service_facts ad-hoc to explore a host's facts before you reference them in templates or conditionals, rather than guessing at fact names.
  • Double-quote the -a argument string so the shell hands the module one intact set of key=value pairs, not a shredded one.
Comparable tools ssh host "systemctl restart nginx" in a for loop, the unstructured equivalent salt '*' service.restart nginx Salt's remote-execution analog knife ssh ad-hoc against a Chef fleet Terraform no equivalent — it converges resources, not one-off host commands

Knowledge Check

When is an ad-hoc command the right tool rather than a playbook?

  • For a genuine one-off — gathering a fact, restarting a stuck service — that will not run again
  • For the standard production deployment that the whole team runs by hand on every single release
  • For any production change that has to be code-reviewed and signed off well before it ever ships
  • For building a brand-new server's full configuration from scratch, one ordered step at a time

What does --become change about an ad-hoc run?

  • It runs the module under sudo on the node, so root-owned operations succeed instead of failing
  • It makes the whole command idempotent by checking the live host's state before it acts on it
  • It widens the host pattern to include every reachable host listed across the whole inventory
  • It records the full command to a central server log so that the entire run becomes auditable later

Why is all the dangerous pattern for a state-changing ad-hoc command?

  • It fires immediately on every host with no plan to review, so "restart one thing" hits the whole fleet
  • It is much slower than a narrow pattern because Ansible has to open a connection to far more hosts
  • It silently skips over every host that happens to be unreachable, quietly hiding those failures from you
  • It disables privilege escalation for the entire run, so the command fails on any root-owned resource

Why prefer -m service -a "name=nginx state=restarted" over -m shell -a "systemctl restart nginx" even for a one-off?

  • The real module is idempotent and reports change honestly, while the shell command discards both for no gain
  • The shell module is simply incapable of restarting a system service like nginx at all
  • The service module restarts the unit cleanly without ever needing --become at all
  • The shell module ignores the targeting host pattern entirely and runs the command on the control node instead

You got correct