Ansible

Ansible Ad-Hoc Commands: Quick Tasks Without Playbooks

Not everything needs a playbook. Sometimes you need to check disk space across 50 servers, restart a stubborn service, or create a user account right now. Ansible ad-hoc commands handle these one-off tasks from a single line in your terminal, no YAML required.

Original content from computingforgeeks.com - post 165256

Ad-hoc commands use the same modules that playbooks do, which means you get idempotency and proper error handling without writing a single file. They’re perfect for troubleshooting, quick audits, and those “just do this on all servers” moments that come up daily in operations. This guide covers the full range of ad-hoc usage: running commands, managing files, installing packages, controlling services, and handling users across mixed RHEL and Debian environments.

Tested April 2026 on Rocky Linux 10.1 and Ubuntu 24.04 LTS with ansible-core 2.16.14

Prerequisites

Before running ad-hoc commands, you need a working Ansible setup:

  • Ansible installed on your control node (install Ansible on Rocky Linux / Ubuntu)
  • An inventory file with your managed hosts defined
  • SSH key authentication configured between the control node and all managed hosts
  • Tested on: Rocky Linux 10.1 (ansible-core 2.16.14, Python 3.12.11), Ubuntu 24.04 (Python 3.12.3)

The examples below use this inventory file with two groups:

cat inventory

Contents of the inventory:

[webservers]
web01 ansible_host=10.0.1.20

[dbservers]
db01 ansible_host=10.0.1.30

[all:vars]
ansible_user=root

Ad-Hoc Command Syntax

Every ad-hoc command follows this structure:

ansible <pattern> -i <inventory> -m <module> -a "<arguments>" [options]

Here’s what each part does:

  • pattern: target hosts or groups (all, webservers, db01, or patterns like web*)
  • -i: path to your inventory file
  • -m: the Ansible module to use (ping, command, copy, etc.)
  • -a: arguments passed to the module
  • -b: become root via sudo (equivalent to --become)
  • -f: number of parallel forks (default is 5)

If you omit -m, Ansible defaults to the command module. So ansible all -i inventory -a "uptime" is the same as explicitly specifying -m command.

Connectivity Check with ping

The first thing to test on any new Ansible setup is connectivity. The ping module verifies that Ansible can reach each host, authenticate via SSH, and execute Python on the remote side.

ansible all -i inventory -m ping

Both hosts should return pong:

web01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
db01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

This is not an ICMP ping. Ansible’s ping module connects over SSH, runs a small Python snippet, and returns pong if everything works. If a host fails, check SSH connectivity, Python availability, and your inventory spelling before looking deeper.

Running Shell Commands

Ansible provides three modules for running commands on remote hosts, and picking the right one matters.

The command Module

The command module executes a binary directly without invoking a shell. It’s the safest option because it avoids shell injection risks.

ansible webservers -i inventory -m command -a "uptime"

The output shows the system uptime on web01:

web01 | CHANGED | rc=0 >>
 23:50:43 up  5 min,  1 user,  load average: 0.00, 0.02, 0.00

Because command is the default module, you can shorten this to:

ansible webservers -i inventory -a "uptime"

One critical limitation: the command module does not support shell operators like pipes (|), semicolons (;), ampersands (&&), or redirects (>). If your command needs any of these, use the shell module instead.

The shell Module

When you need pipes, variable expansion, or chained commands, switch to shell. It runs commands through /bin/sh on the remote host.

ansible all -i inventory -m shell -a "df -h / | tail -1"

The output shows root filesystem usage on both hosts:

web01 | CHANGED | rc=0 >>
/dev/vda2        19G  1.9G   16G  11% /
db01 | CHANGED | rc=0 >>
/dev/vda2        19G  1.3G   17G   7% /

The shell module is more flexible but less secure. It’s susceptible to shell injection if you pass untrusted input as arguments. Use command as your default and reach for shell only when you actually need shell features.

The raw Module

The raw module sends commands over SSH without requiring Python on the remote host. This is useful for bootstrapping fresh systems or managing network devices that don’t have Python installed.

ansible all -i inventory -m raw -a "cat /etc/os-release | head -2"

For day-to-day operations, you’ll rarely need raw. Stick with command or shell unless you’re working with hosts that lack Python.

Gathering System Information

Ansible’s setup module collects detailed facts about each host. These are the same facts available in playbooks via ansible_facts, but you can query them directly from the command line.

Pull OS distribution details from all hosts:

ansible all -i inventory -m setup -a "filter=ansible_distribution*"

This returns the distribution name, version, and release for each host:

web01 | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "24",
        "ansible_distribution_release": "noble",
        "ansible_distribution_version": "24.04"
    },
    "changed": false
}
db01 | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Rocky",
        "ansible_distribution_file_variety": "RedHat",
        "ansible_distribution_major_version": "10",
        "ansible_distribution_release": "Blue Onyx",
        "ansible_distribution_version": "10.1"
    },
    "changed": false
}

Check memory across your fleet:

ansible all -i inventory -m setup -a "filter=ansible_memtotal_mb"

You can also dump all facts to local JSON files for offline analysis:

ansible all -i inventory -m setup --tree /tmp/facts

This creates one JSON file per host in /tmp/facts/. Useful when you need to audit hardware specs or OS versions across a large environment.

File Operations

Managing files across multiple servers is one of the most common ad-hoc use cases. Ansible provides several modules for this.

Copying Content with the copy Module

Push inline content to a file on all hosts:

ansible all -i inventory -m copy -a "content='Hello from Ansible\n' dest=/tmp/ansible-test.txt" -b

Ansible creates the file and reports the checksum:

web01 | CHANGED => {
    "changed": true,
    "checksum": "fd69a468413c98da1b8b640e57fb2eab6dfaa5b1",
    "dest": "/tmp/ansible-test.txt",
    "gid": 0,
    "group": "root",
    "md5sum": "2d8e1e0e17bade39e4b559a36ae29498",
    "mode": "0644",
    "owner": "root",
    "size": 21,
    "src": "/root/.ansible/tmp/ansible-tmp-1743724215.23-1234-56789/source",
    "state": "file",
    "uid": 0
}

To copy an existing local file to all hosts:

ansible all -i inventory -m copy -a "src=/etc/hosts dest=/tmp/hosts-backup"

The copy module handles ownership, permissions, and idempotency. If the file already exists with the same content, Ansible skips the transfer and reports "changed": false.

Managing Files and Directories with the file Module

Create a directory on all hosts:

ansible all -i inventory -m file -a "path=/opt/app state=directory mode=0755 owner=root" -b

Both hosts confirm the directory was created. On Rocky, the output includes SELinux context information (unconfined_u:object_r:usr_t:s0), which is expected.

Adjust permissions on an existing file:

ansible all -i inventory -m file -a "path=/tmp/ansible-test.txt mode=0644"

Remove a file or directory:

ansible all -i inventory -m file -a "path=/tmp/ansible-test.txt state=absent" -b

Setting state=absent deletes the target. Ansible won’t complain if it’s already gone.

Pulling Files with fetch

The fetch module works in reverse: it pulls files from managed hosts to the control node.

ansible all -i inventory -m fetch -a "src=/etc/hostname dest=/tmp/fetched/"

Ansible saves each file under a host-specific subdirectory (/tmp/fetched/web01/etc/hostname, /tmp/fetched/db01/etc/hostname) so files from different hosts don’t overwrite each other.

Package Management

Installing and removing packages across mixed environments requires the right module for each OS family.

Debian/Ubuntu: the apt Module

Install Nginx on Ubuntu hosts:

ansible webservers -i inventory -m apt -a "name=nginx state=present update_cache=yes" -b

The update_cache=yes flag runs apt update before installing, so you’re pulling from the latest package index. Ansible installed nginx 1.24.0-2ubuntu7 on our Ubuntu 24.04 test host.

Remove a package when you’re done:

ansible webservers -i inventory -m apt -a "name=nginx state=absent" -b

RHEL/Rocky: the dnf Module

Install Apache on Rocky Linux hosts:

ansible dbservers -i inventory -m dnf -a "name=httpd state=present" -b

This installed httpd-2.4.63 on our Rocky 10.1 test server. Remove it with:

ansible dbservers -i inventory -m dnf -a "name=httpd state=absent" -b

Cross-Platform: the package Module

When you target mixed environments, the package module auto-detects whether to use apt, dnf, or whatever package manager the system provides.

ansible all -i inventory -m package -a "name=curl state=present" -b

The trade-off: package doesn’t support module-specific options like update_cache for apt or enablerepo for dnf. Use it for simple installs across mixed fleets, and fall back to the OS-specific modules when you need finer control.

Service Management

Starting, stopping, and restarting services is where ad-hoc commands earn their keep during troubleshooting.

Start and enable sshd on Rocky Linux hosts:

ansible dbservers -i inventory -m service -a "name=sshd state=started enabled=yes" -b

This works on Rocky because the SSH service is called sshd. On Ubuntu, the service name is ssh (without the trailing d). Targeting Ubuntu hosts with name=sshd will fail with “Could not find the requested service sshd.” This catches most people off guard in mixed environments.

Restart Nginx after a config change:

ansible webservers -i inventory -m service -a "name=nginx state=restarted" -b

For more granular systemd control (masking units, setting daemon-reload), use the systemd module:

ansible all -i inventory -m systemd -a "name=sshd state=restarted daemon_reload=yes" -b

The systemd module gives you access to features that the generic service module doesn’t expose, like triggering a daemon reload before restarting.

User and Group Management

Creating and managing user accounts across servers is another task that’s faster with ad-hoc commands than writing a playbook.

Create a deploy user on all hosts:

ansible all -i inventory -m user -a "name=deploy state=present shell=/bin/bash" -b

Verify the user exists:

ansible all -i inventory -m command -a "id deploy"

Both hosts show uid=1001:

web01 | CHANGED | rc=0 >>
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy)
db01 | CHANGED | rc=0 >>
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy)

Add an SSH public key for the new user so they can log in without a password:

ansible all -i inventory -m authorized_key -a "user=deploy key='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyHere deploy@control' state=present" -b

When a user account is no longer needed, remove it along with its home directory:

ansible all -i inventory -m user -a "name=deploy state=absent remove=yes" -b

The remove=yes flag deletes the home directory and mail spool. Without it, Ansible removes the user from /etc/passwd but leaves files behind.

Controlling Parallelism

Ansible runs tasks in parallel across hosts. The default is 5 simultaneous connections (forks), which is fine for small environments but becomes a bottleneck when you manage hundreds of servers.

Run commands sequentially (one host at a time):

time ansible all -i inventory -m shell -a "sleep 2 && hostname" -f 1

With -f 1, Ansible finishes one host before starting the next, so the total time is roughly the sum of all hosts.

Now bump it to 10 forks:

time ansible all -i inventory -m shell -a "sleep 2 && hostname" -f 10

Both hosts complete in about 2.98 seconds total because they run in parallel. With only 2 hosts the difference is small, but at 100+ hosts the speedup is dramatic.

For large production environments, set forks permanently in ansible.cfg:

sudo vi /etc/ansible/ansible.cfg

Set the forks value under the [defaults] section:

[defaults]
forks = 20

Be careful with very high fork counts. Each fork opens an SSH connection and consumes memory on the control node. Start with 20 and increase based on your hardware.

Privilege Escalation

Most system administration tasks require root privileges. Ansible handles this through privilege escalation, commonly called “become.”

The -b flag (short for --become) runs the command as root via sudo:

ansible all -i inventory -m apt -a "name=nginx state=present" -b

To become a specific user instead of root, use --become-user:

ansible dbservers -i inventory -m shell -a "psql -c 'SELECT version();'" -b --become-user=postgres

If sudo requires a password on your systems, add --ask-become-pass (or -K), and Ansible will prompt you before running:

ansible all -i inventory -m command -a "whoami" -b --ask-become-pass

For an entire project, configure privilege escalation defaults in ansible.cfg to avoid typing -b on every command:

sudo vi ansible.cfg

Add the privilege escalation section:

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

With these defaults, every ad-hoc command and playbook runs with sudo automatically.

Useful Ad-Hoc One-Liners

Here are practical one-liners that come up regularly in production environments:

Check disk space across all servers:

ansible all -i inventory -m shell -a "df -h / | tail -1"

Find files larger than 500MB:

ansible all -i inventory -m shell -a "find / -xdev -type f -size +500M -exec ls -lh {} \;" -b

Check if a specific service is running:

ansible all -i inventory -m shell -a "systemctl is-active nginx || echo 'not running'"

Read a specific config value from all hosts:

ansible all -i inventory -m shell -a "grep '^PermitRootLogin' /etc/ssh/sshd_config"

Check listening ports:

ansible all -i inventory -m shell -a "ss -tlnp" -b

Restart a service on a specific subset of hosts using a limit:

ansible webservers -i inventory -m service -a "name=nginx state=restarted" -b --limit web01

Check system uptime and load across the fleet:

ansible all -i inventory -a "uptime"

These one-liners handle 90% of the quick tasks that would otherwise require SSH-ing into each server individually.

command vs shell vs raw

Choosing the right execution module prevents subtle bugs and security issues. Here’s how they compare:

Featurecommandshellraw
Pipes and redirectsNoYesYes
Environment variablesNoYesYes
Requires PythonYesYesNo
Shell injection riskNonePossiblePossible
Best forDefault choice, simple commandsPipes, chained commands, grep patternsBootstrap, network devices, no Python

Start with command for everything. Switch to shell when you need pipes or variable expansion. Reserve raw for edge cases where Python isn’t available on the target system.

One more thing: if you find yourself chaining more than two or three commands together in a shell call, that’s a sign you should write a playbook instead. Ad-hoc commands are for quick, focused tasks. Once the logic gets complex, YAML is your friend.

Related Articles

Automation Automated installation of Debian 12 using PXE Boot Automation 4 Common Automation Testing Mistakes You Must Avoid Automation Migrating GitLab from RHEL 6 TO RHEL 7/CentOS 7 Automation Run GitLab CE in Docker with Docker Compose

Leave a Comment

Press ESC to close