Skip to content

Ansible Ad Hoc Commands


Before we touch playbooks, let’s do real work with a single command. Ad hoc commands let you run one task against your inventory instantly — no file to write, no structure to plan out, just a single line that does something right now. That immediacy is exactly the point of this chapter: you’ll run the same handful of small tasks here that we’ll come back to once playbooks enter the picture, so you can feel directly why repeating them by hand starts to hurt — and why playbooks exist at all.

What Is An Ad Hoc Command?

An ad hoc command runs a single Ansible module against a set of hosts, directly from the command line, using the ansible command (not ansible-playbook, which we’ll get to later). There’s nothing to save, version, or reuse — you type it, it runs, and it’s done. That’s a feature for quick one-off checks, and a limitation the moment you need to do more than one thing at a time.

The Inventory We’re Using

We’ll run every example in this chapter against the grouped inventory built in the previous chapter:

inventory.yaml
servers:
  hosts:
    ubuntu:
      ansible_host: 10.0.0.1
      ansible_user: alice
    fedora:
      ansible_host: 10.0.0.2
      ansible_user: alice

If you’ve still got this saved as inventory.yaml from the last chapter, you’re ready to go.

Basic Ad Hoc Syntax

ansible <pattern> -i inventory.yaml -m <module> -a '<module arguments>'
  • <pattern> is which hosts to target — a single host name, a group name, or all.
  • -i points at the inventory file to use.
  • -m names the module to run.
  • -a passes arguments to that module, as a single quoted string (to prevent any prior shell expansion).

Sanity-Checking Connectivity With ansible.builtin.ping

Note

Before running any command below, add your key (alice) in ssh-agent:

eval $(ssh-agent -s)
ssh-add alice

If, for some reason, you are not comfortable with this, you can pass --key-file alice for every ad-hoc command you execute.

The very first thing worth running against any inventory, new or old, is ansible.builtin.ping:

ansible servers -i inventory.yaml -m ansible.builtin.ping

Note

If you get error like Host key verification failed., you can bypass this by:

export ANSIBLE_HOST_KEY_CHECKING=False

This is okay for lab environment, not intended for production.

ubuntu | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
fedora | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

This isn’t an ICMP ping — it doesn’t test raw network reachability the way the ping command you already know does. It confirms something more specific and more useful: that Ansible can actually SSH into the host, and that a working Python interpreter is there waiting for it, exactly the requirement flagged back in the installation chapter. A pong back means the whole chain — SSH, auth, Python — is genuinely working end to end.

Running Commands With ansible.builtin.command

ansible.builtin.command runs a program directly on the remote host, without invoking a shell:

ansible servers -i inventory.yaml -m ansible.builtin.command -a "uptime"
ubuntu | CHANGED | rc=0 >>
 14:32:01 up 2 days,  3:14,  0 users,  load average: 0.00, 0.01, 0.05
fedora | CHANGED | rc=0 >>
 14:32:02 up 2 days,  3:11,  0 users,  load average: 0.01, 0.02, 0.04

“Without invoking a shell” matters more than it might sound like at first. Try a command that relies on shell features, like a pipe:

ansible servers -i inventory.yaml -m ansible.builtin.command -a "echo hello | tr a-z A-Z"
ubuntu | CHANGED | rc=0 >>
hello | tr a-z A-Z

That’s not a mistake — command passed hello, |, tr, a-z, and A-Z to echo as four literal arguments, since there’s no shell around to interpret the | as a pipe at all. echo printed exactly what it was given.

command vs ansible.builtin.shell: When You Actually Need A Shell

ansible.builtin.shell runs your command through an actual shell on the remote host, so shell features like pipes, redirection, and variable expansion all work as expected:

ansible servers -i inventory.yaml -m ansible.builtin.shell -a "echo hello | tr a-z A-Z"
ubuntu | CHANGED | rc=0 >>
HELLO
fedora | CHANGED | rc=0 >>
HELLO

Same input string, genuinely different result — this time the pipe did what it looked like it should. The practical rule: reach for command by default, since it’s simpler and more predictable, and switch to shell specifically when you need something command genuinely can’t do, like piping, redirecting, or chaining commands together.

Printing Messages With ansible.builtin.debug

ansible.builtin.debug prints a message or a value — invaluable for checking what’s actually going on, both here and constantly once we’re writing playbooks:

ansible servers -i inventory.yaml -m ansible.builtin.debug -a "msg='Hello from ad hoc'"
ubuntu | SUCCESS => {
    "msg": "Hello from ad hoc"
}
fedora | SUCCESS => {
    "msg": "Hello from ad hoc"
}

Note the single quotes wrapped around the message value, sitting inside the outer double quotes — -a takes key=value pairs as a single string, so a value containing spaces needs its own quoting to stay together as one piece.

Targeting Hosts vs Groups vs Everything

The <pattern> in every command above has been servers, but it doesn’t have to be:

ansible ubuntu -i inventory.yaml -m ansible.builtin.ping
ansible fedora -i inventory.yaml -m ansible.builtin.ping
ansible all -i inventory.yaml -m ansible.builtin.ping

The first two target a single host each, by name; the last targets every host in the inventory, using the built-in all group from the previous chapter.

Note

You might notice ansible localhost -m ansible.builtin.ping also works, with no inventory file at all. There’s a real reason for that — covered fully in the next chapter.

Run Without Inventory: Trailing Comma

You can run an Ansible ad-hoc command against a single IP without an inventory file by using a comma after the IP address:

ansible all -i "192.168.1.10," -m ping

The trailing comma is important—it tells Ansible this is a host list, not an inventory filename.

Where Ad Hoc Commands Start To Hurt

Ad hoc commands are great for tasks you repeat rarely like may be you are turning off your server in the weekend, etc. But imagine you needed to run all four of the commands above, in order, on a fresh host, every time you set one up. You’d need to remember the exact module names, get every flag and quote right, run them one at a time, and hope you didn’t typo anything three commands in. Nothing here is saved anywhere — do it again tomorrow, and you’re retyping the whole sequence from memory or scrollback.

That’s the exact gap playbooks close: the same tasks, written once, saved as a file, and run reliably as a single repeatable unit — as many times, on as many hosts, as you need. We’ll come back to this exact set of tasks once we get there.

Best Practices

  • Run ansible.builtin.ping first, against any inventory, before trying anything else — it’s the fastest way to confirm the whole connection chain actually works.
  • Default to command, not shell, unless you specifically need a shell feature — it’s more predictable, and it doesn’t quietly depend on whatever shell happens to be configured on the remote host.
  • Use ansible.builtin. prefixed module names (the fully-qualified form) rather than the bare ping, command, shell, debug names. The short forms still work today, but the explicit form is the modern convention and stays unambiguous once other collections are installed alongside Ansible’s built-in ones.
  • Reach for a playbook, not a longer ad hoc command, the moment you find yourself wanting to run more than one task in sequence.
Last updated on