Ansible Ad-Hoc Commands and Core Modules
An ad-hoc command is one module against a pattern of hosts, right now, with no playbook. It is how you investigate, how you fix one thing quickly, and how you learn what a module does before committing it to a file. This covers the shape of the command line, the six modules that cover most ad-hoc work, and the parallelism that is running underneath.
Ad-hoc Commands and Modules Guide 6 of 45 Beginner
- OSUbuntu 26.04 LTS
- ansible-core2.20.1
- Python3.14.4
- TimeAbout 14 min
- Reviewed23 August 2026
Written against the versions above. Module names are written here in full - `ansible.builtin.command` rather than `command`. The short name still works and always will for `ansible.builtin`, but the fully qualified name is unambiguous when several collections define something similar, and it is what current documentation uses.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| ANS-CTL01 | 192.168.0.36 | Ubuntu 26.04 LTS | Ansible Control Node | 2 Core | 3 GB | 50 GB |
| ANS-A01 | 192.168.0.37 | Ubuntu 26.04 LTS | Managed Node (group: web) | 2 Core | 3 GB | 50 GB |
| ANS-B01 | 192.168.0.38 | Ubuntu 26.04 LTS | Managed Node (group: db) | 2 Core | 3 GB | 50 GB |
Before you start
- A working control node - see guide 5.
- The session creates and removes
/tmp/adhoc-demoon both managed nodes and ensurescronis running.
-
The configuration this guide assumes
Everything below runs from the home directory on ANS-CTL01, where two files sit side by side.
~/ansible.cfg- found because Ansible looks for./ansible.cfgin the current directory:[defaults] inventory = ./inventory.ini host_key_checking = False remote_user = sysadmin result_format = yamlresult_format = yamlrather than thestdout_callback = yamlthat older material recommends: that callback lived incommunity.generaland was removed in version 12.0.0, so a playbook run against this config errors out with "The 'community.general.yaml' callback plugin has been removed." The built-in option replaces it and produces the same readable output.~/inventory.ini- two groups and a parent group, so host patterns have something to select:[web] ans-a01 [db] ans-b01 [production:children] web db [web:vars] app_port=8080 [all:vars] ansible_python_interpreter=/usr/bin/python3The short names resolve because
/etc/hostson all three machines carries them, and the control node reaches both managed nodes with an ed25519 key and passwordless sudo.If any of this is unfamiliar, build it first: guide 3, guide 4 and guide 5 cover the three pieces in order.
The last two commands are the check worth running before anything else in this path:
pongfrom both hosts, androotwhen escalation is on.bash Example session cat ~/ansible.cfg[defaults]inventory = ./inventory.inihost_key_checking = Falseremote_user = sysadminresult_format = yamlcat ~/inventory.ini[web]ans-a01 [db]ans-b01 [production:children]webdb [web:vars]app_port=8080 [all:vars]ansible_python_interpreter=/usr/bin/python3grep -E "ans-(ctl|a|b)01" /etc/hosts192.168.0.36 ans-ctl01192.168.0.37 ans-a01192.168.0.38 ans-b01ls -l ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub-rw------- 1 sysadmin sysadmin 411 Aug 23 07:06 /home/sysadmin/.ssh/id_ed25519-rw-r--r-- 1 sysadmin sysadmin 99 Aug 23 07:06 /home/sysadmin/.ssh/id_ed25519.pubansible-inventory --graph@all: |--@ungrouped: |--@production: | |--@web: | | |--ans-a01 | |--@db: | | |--ans-b01ansible all -m ansible.builtin.pingans-a01 | SUCCESS => { "changed": false, "ping": "pong"}ans-b01 | SUCCESS => { "changed": false, "ping": "pong"}ansible all -m ansible.builtin.command -a "id -un" --becomeans-a01 | CHANGED | rc=0 >>rootans-b01 | CHANGED | rc=0 >>rootExpected resultBoth config files as shown, and
pongplusrootfrom both managed nodes.Success conditionYour control node matches the one every command in this guide was run on.
-
The shape of an ad-hoc command
Three parts, always:
ansible <pattern> -m <module> -a "<arguments>"who, what, with what. That is the whole grammar.
ans-a01 | SUCCESS => { "changed": false, "ping": "pong" }Read the first word of each result.
SUCCESS,CHANGED,FAILED,UNREACHABLE,SKIPPED- five states, and the difference between the first two is the entire idea of idempotence.setupis the module that returns facts, andfilter=is essential - unfiltered it returns several hundred values per host:"ansible_hostname": "ans-a01", "ansible_distribution": "Ubuntu", "ansible_memtotal_mb": 2932Those names are exactly what you use later in a template or a
when:.bash Example session ansible all -m ansible.builtin.pingans-a01 | SUCCESS => { "changed": false, "ping": "pong"}ans-b01 | SUCCESS => { "changed": false, "ping": "pong"}ansible web -m ansible.builtin.command -a "uptime -p"ans-a01 | CHANGED | rc=0 >>up 1 hour, 57 minutesansible all -m ansible.builtin.setup -a "filter=ansible_hostname,ansible_distribution,ansible_memtotal_mb"ans-a01 | SUCCESS => { "ansible_facts": { "ansible_distribution": "Ubuntu", "ansible_hostname": "ans-a01", "ansible_memtotal_mb": 3398 }, "changed": false}ans-b01 | SUCCESS => { "ansible_facts": { "ansible_distribution": "Ubuntu", "ansible_hostname": "ans-b01", "ansible_memtotal_mb": 3398 }, "changed": false}Expected resultA pong, an uptime, and three named facts.
Success conditionYou can run any module against any pattern from memory.
-
The handful you will actually type
-m ansible.builtin.file -a "path=... state=directory mode=0755" -m ansible.builtin.copy -a "dest=... content='...' mode=0644" -m ansible.builtin.stat -a "path=..."filemanages existence, type and permissions;copyputs content there;stattells you what is already there without changing anything.Note the argument style:
key=valuepairs separated by spaces, all inside one-astring. It looks odd next to YAML, and it is the same set of parameters the module takes in a playbook.statis the one people forget, and it is the right tool for 'is this already done' - it never changes anything, so it is safe to run anywhere.bash Example session ansible all -m ansible.builtin.file -a "path=/tmp/adhoc-demo state=directory mode=0755"ans-a01 | CHANGED => { "changed": true, "gid": 1000, "group": "sysadmin", "mode": "0755", "owner": "sysadmin", "path": "/tmp/adhoc-demo", "size": 40, "state": "directory", "uid": 1000}ans-b01 | CHANGED => { "changed": true, "gid": 1000, "group": "sysadmin", "mode": "0755", "owner": "sysadmin", "path": "/tmp/adhoc-demo", "size": 40, "state": "directory", "uid": 1000}ansible all -m ansible.builtin.copy -a "dest=/tmp/adhoc-demo/note.txt content='written by an ad-hoc command\n' mode=0644"ans-a01 | CHANGED => { "changed": true, "checksum": "b52424da9dd1eac54020c9c91037734abf5f4fa9", "dest": "/tmp/adhoc-demo/note.txt", "gid": 1000, "group": "sysadmin", "md5sum": "30e60d9f4367b6264011e50470ab9b08", "mode": "0644", "owner": "sysadmin", "size": 29, "src": "/home/sysadmin/.ansible/tmp/ansible-tmp-1787471782.402282-11978-35299567800357/.source.txt", "state": "file", "uid": 1000}ans-b01 | CHANGED => { "changed": true, "checksum": "b52424da9dd1eac54020c9c91037734abf5f4fa9", "dest": "/tmp/adhoc-demo/note.txt", "gid": 1000, "group": "sysadmin", "md5sum": "30e60d9f4367b6264011e50470ab9b08", "mode": "0644", "owner": "sysadmin", "size": 29, "src": "/home/sysadmin/.ansible/tmp/ansible-tmp-1787471782.4044476-11979-44541526997894/.source.txt", "state": "file", "uid": 1000}ansible all -m ansible.builtin.command -a "cat /tmp/adhoc-demo/note.txt"ans-a01 | CHANGED | rc=0 >>written by an ad-hoc commandans-b01 | CHANGED | rc=0 >>written by an ad-hoc commandansible all -m ansible.builtin.stat -a "path=/tmp/adhoc-demo/note.txt" 2>&1 | grep -E "SUCCESS|\"mode\"|\"size\"|\"exists\""ans-a01 | SUCCESS => { "exists": true, "mode": "0644", "size": 29,ans-b01 | SUCCESS => { "exists": true, "mode": "0644", "size": 29,Expected resultA directory, a file with known content and mode, and a read-only check.
Success conditionYou can create and inspect files across a fleet without a playbook.
-
The ones that need root
service_factsgathers every unit on the host - useful, and large:ansible web -m ansible.builtin.service_factsand
systemd_servicemanages one. Note--become, because managing services needs root:ansible web -m ansible.builtin.systemd_service \ -a "name=cron state=started enabled=true" --becomeThe two parameters are independent and both matter:
state=startedis now,enabled=trueis after a reboot. Setting one and assuming the other is a classic way to lose a service on the next restart.The result reports
okrather thanchangedhere, because cron was already running and already enabled - the module checked and did nothing, which is what you want.bash Example session ansible web -m ansible.builtin.service_facts 2>&1 | grep -c "\.service"634ansible web -m ansible.builtin.systemd_service -a "name=cron state=started enabled=true" --become 2>&1 | grep -E "SUCCESS|CHANGED|\"state\"|\"enabled\""ans-a01 | SUCCESS => { "enabled": true, "state": "started", "UnitFilePreset": "enabled", "UnitFileState": "enabled",ansible web -m ansible.builtin.command -a "systemctl is-active cron"ans-a01 | CHANGED | rc=0 >>activeExpected resultA unit count, an idempotent service task, and confirmation.
Success conditionYou can manage services across hosts in one line.
-
How many at once
DEFAULT_FORKS(default) = 5Five hosts in parallel by default. And it is real - two hosts each sleeping two seconds:
ansible all -m ansible.builtin.command -a "sleep 2" 0.33s user 0.08s system 16% cpu 2.479 total2.479 seconds, not 4. They ran at the same time.
This is the number to raise on a large inventory - 5 forks against 200 hosts means 40 sequential batches. It is also the number to *lower* when a task is heavy on the control node, since every fork is a process there.
When to stop using ad-hoc commands: as soon as you need a second step, a condition, or to do the same thing again next week. Ad-hoc is for investigation and one-offs; anything you would repeat belongs in a playbook.
bash Example session ansible-config dump | grep -E "^DEFAULT_FORKS"DEFAULT_FORKS(default) = 5time ansible all -m ansible.builtin.command -a "sleep 2" 2>&1 | tail -4ans-a01 | CHANGED | rc=0 >> ans-b01 | CHANGED | rc=0 >> ansible all -m ansible.builtin.command -a "sleep 2" 2>&1 0.33s user 0.08s system 16% cpu 2.479 totaltail -4 0.00s user 0.00s system 0% cpu 2.481 totalansible all -m ansible.builtin.file -a "path=/tmp/adhoc-demo state=absent"ans-a01 | CHANGED => { "changed": true, "path": "/tmp/adhoc-demo", "state": "absent"}ans-b01 | CHANGED => { "changed": true, "path": "/tmp/adhoc-demo", "state": "absent"}Expected resultFive forks by default, and two hosts finishing in the time of one.
Success conditionYou know how wide Ansible runs, and how to change it.
Troubleshooting
Missing target hostsor no output at all.Why: The pattern matched nothing.
Fix:
--list-hostsfirst. A non-matching pattern is a warning, not an error.A task fails with a permissions error.
Why: No privilege escalation.
Fix:Add
--become.-a "key=value"is rejected.Why: Quoting - the whole argument string must be one shell argument.
Fix:Wrap it all in one set of quotes, and use single quotes outside if the value needs doubles.
A large inventory takes far too long.
Why: The default of 5 forks.
Fix:
-f 50, orforksinansible.cfg. Watch control-node load.