Using Ansible ad-hoc commands for daily operational tasks: checking uptime, managing files and packages, restarting services, and pushing SSH keys to many servers with just a single command line.

After episode 3, where we covered how to manage inventory — from INI and YAML formats, host grouping, host patterns, to dynamic inventory — in this episode we will put that inventory to use for real work: Ad-Hoc Commands.
Ad-hoc commands are Ansible's fastest and most concise mode. If you're the type who prefers to "ask first rather than write", ad-hoc is the answer: just one line of command, and dozens of servers respond instantly. In daily operations, this kind of moment happens very often — for example, at 2 AM there's an alert that a server's disk is full, and you just need to check 40 servers at once without writing a playbook.
But keep in mind: ad-hoc is not a substitute for playbooks. It's a pocket knife — fast for one-off tasks, but not the tool for repetitive work that needs to be documented. At the end of this episode, we'll see when to move on to playbooks.
Simply put, an ad-hoc command is an Ansible command run directly from the terminal without a playbook file. It uses the inventory you prepared in episode 3, then executes a single module against the targeted hosts.
Important
When is ad-hoc used? When the task is one-off, exploratory, or a response to an emergency — like checking disks, restarting a service, or checking the kernel version. Tasks that must run repeatedly and be managed with a team should be written as a playbook (episode 5).
The basic structure of an ad-hoc command is:
ansible <host-pattern> -m <module_name> -a "<module_args>"Let's break down each part:
<host-pattern> — the target servers, exactly like the host patterns we covered in episode 3 (all, webservers, webservers:&staging, and so on).-m <module_name> — specifies the module to use. Modules are the "weapon" that determines what Ansible does on the target servers.-a "<module_args>" — module arguments, containing module-specific parameters.There are also common flags you will use very often:
| Flag | Function | Example |
|---|---|---|
-i / --inventory | Specifies the inventory file (static or dynamic) | -i inventory.yml |
-b / --become | Runs with privilege escalation (sudo) | -b |
-K / --ask-become-pass | Prompts for the sudo password interactively | -bK |
-u / --user | Specifies the SSH user to use | -u deploy |
--check | Dry run — only reports, changes nothing | --check |
-v / -vvv | Adds more verbosity | -vvv |
The simplest example: testing the connection to all hosts in the inventory.
ansible all -i inventory.yml -m ansible.builtin.pingweb-01.prod.example.com | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
db-01.prod.example.com | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}Each host answers pong — meaning the SSH connection, authentication, and Python interpreter all work. Notice the output is in JSON, because that's the standard format for Ansible module execution results.
There's a set of modules you'll almost certainly use every day. Let's go through them one by one.
Besides ansible.builtin.ping, which we already tried above, another module you must know is ansible.builtin.setup — the module that gathers facts (system information) from every host.
ansible all -i inventory.yml -m ansible.builtin.setup -a "filter=ansible_distribution*"web-01.prod.example.com | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Ubuntu",
"ansible_distribution_file_parsed": true,
"ansible_distribution_major_version": "24",
"ansible_distribution_release": "noble",
"ansible_distribution_version": "24.04",
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false
}Without filter, the setup output is very long (containing CPU, RAM, all network interfaces, mounts, and more) — that's normal, because this module collects everything. The filter parameter helps narrow down the output when you only need a specific part.
Tip
In episode 7, we will learn to leverage these facts inside playbooks — for example, determining the package manager based on ansible_os_family, or creating configuration that adapts to the server's RAM size. For now, just get familiar with calling it via ad-hoc.
These are the trio of modules most often misunderstood. All three execute commands, but in different ways:
| Aspect | ansible.builtin.command | ansible.builtin.shell | ansible.builtin.raw |
|---|---|---|---|
Shell features (pipes, &&, env vars, redirection) | ❌ Not supported | ✅ Fully supported | ✅ Fully supported |
| Needs Python on the target host | ✅ Yes | ✅ Yes | ❌ No — goes through SSH directly |
| Security (avoids shell injection) | ✅ Safest | ⚠️ Needs caution | ⚠️ Needs caution |
| Built-in idempotency | ❌ No | ❌ No | ❌ No |
| When to use | Simple commands without shell features | Commands with pipes/loops/variables | Hosts without Python, network devices, recovery |
Real examples of all three:
ansible all -i inventory.yml -m ansible.builtin.command -a "uptime"Warning
The shell and raw modules are very powerful, but also very easy to misuse. Because they support pipes and shell variables, there's a risk of command injection — especially with dynamic input. The rule of thumb: use command first, then shell only if you really need shell features, and stay away from raw except for special cases (hosts without Python or recovery mode).
To create directories, touch files, or set permissions, use ansible.builtin.file:
ansible all -i inventory.yml -b -m ansible.builtin.file -a "path=/var/www/blog state=directory owner=deploy group=www-data mode=0755"The state argument determines the desired result:
state | Effect |
|---|---|
directory | Creates the directory along with its parents |
touch | Creates an empty file if it doesn't exist |
file / link | A regular file or symlink |
absent | Deletes a file/directory |
To push a file from the control node to the target servers, use ansible.builtin.copy:
ansible all -i inventory.yml -b -m ansible.builtin.copy -a "src=/etc/nginx/sites-available/blog.conf dest=/etc/nginx/sites-available/blog.conf owner=root group=root mode=0644"This scenario is very useful when you want to distribute the same configuration file to many servers at once.
Installing, updating, or removing packages is the most common daily task. The module depends on the distro: ansible.builtin.apt for Debian/Ubuntu, ansible.builtin.dnf for RHEL/Rocky/Alma, and ansible.builtin.yum for older RHEL.
ansible all -i inventory.yml -b -m ansible.builtin.apt -a "name=nginx state=present update_cache=yes"The state values for package modules:
state | Meaning |
|---|---|
present | Make sure it's installed (install if not) — idempotent |
latest | Install and update to the latest version |
absent | Remove the package |
installed / removed | Old synonyms (deprecated in some versions) |
Tip
Notice update_cache=yes in the apt example — this is equivalent to apt update before installing. Without it, the server's package index can go stale and the package may not be found.
Manage services (start, stop, restart) with ansible.builtin.systemd_service for modern systems that use systemd:
ansible web -i inventory.yml -b -m ansible.builtin.systemd_service -a "name=nginx state=restarted"The state parameter for service modules: started, stopped, restarted, reloaded. The enabled: true/false parameter controls whether the service starts automatically at boot.
Note
This module is named systemd_service, replacing the old names service and systemd. If you come across a tutorial using -m service, know that this module is now part of an older ansible.builtin version, and systemd_service is its successor for systemd.
Now let's combine everything into scenarios that genuinely happen in the field.
1. Check uptime across all servers
At night, the ops team wants to make sure no server has recently rebooted on its own:
ansible all -i inventory.yml -m ansible.builtin.command -a "uptime"web-01.prod.example.com | SUCCESS | rc=0 >>
11:24:35 up 42 days, 3:12, 1 user, load average: 0.31, 0.45, 0.52
web-02.prod.example.com | SUCCESS | rc=0 >>
11:24:35 up 3 min, 1 user, load average: 1.10, 1.00, 0.90
db-01.prod.example.com | SUCCESS | rc=0 >>
11:24:35 up 42 days, 3:12, 1 user, load average: 0.10, 0.12, 0.11Notice web-02 has only been up for 3 minutes — a signal that something is wrong on that server. With a single command, you've found an anomaly that needs investigating. Compare that with having to ssh into 3 servers one by one.
2. Rolling reboot
When there's a kernel update or a config change that requires a restart, do a rolling reboot. Although you could use command: reboot, there's a better dedicated module: ansible.builtin.reboot. This module performs the reboot, then waits for the host to come back online and SSH to be ready — so you know exactly when it's safe to move on to the next server.
ansible 'webservers:!web-01.prod.example.com' -i inventory.yml -b -m ansible.builtin.rebootweb-02.prod.example.com | CHANGED => {
"changed": true,
"elapsed": 42,
"rebooted": true
}Caution
For production, don't reboot all servers at once. The pattern above deliberately excludes one server (:!web-01) as a pilot, then moves on to the rest after confirming the first one is healthy again. This concept aligns with the serial strategy that will be covered in episode 15.
3. Emergency SSH key push
A new team member needs access to all servers right now. The ansible.builtin.authorized_key module handles it safely — the key is added to authorized_keys without touching any other file:
ansible all -i inventory.yml -b -m ansible.builtin.authorized_key \
-a "user=deploy state=present key='{{ lookup(\"file\", \"/home/budi/.ssh/id_ed25519.pub\") }}'"Tip
The trick above uses the lookup plugin file to read the public key content directly from the control node — you don't need to manually copy-paste the key content, which is prone to typos. Notice the double quotes inside -a to protect the curly braces from the shell.
--check Mode (Dry Run)Before running a command that modifies the system (like installing packages or restarting services), it's highly recommended to try --check mode first. This mode makes Ansible not actually change anything on the target — it only calculates and reports what would happen.
ansible all -i inventory.yml -b -m ansible.builtin.apt -a "name=nginx state=present" --checkweb-01.prod.example.com | SUCCESS => {
"changed": false,
"msg": "OK"
}
db-01.prod.example.com | SUCCESS => {
"changed": true,
"msg": "If state is 'present', package 'nginx' would be installed"
}Notice two things:
changed: false — nothing will change.changed: true with the message "would be installed" — Ansible tells you the changes it would make without actually making them.Warning
--check is only useful if the module supports it. Modules like apt, file, copy, and most stateful modules support it well. However, the command and shell modules by default do not — because Ansible can't predict the effect of an arbitrary shell command. So don't be surprised if --check + command still appears "as if" it would change.
| Mistake | Symptom | Solution |
|---|---|---|
Using a pipe in command | Error `'/bin/sh: 1: | : not found'` or the command doesn't run |
Forgetting -b for root tasks | Permission denied when installing/restarting services | Add -b (and -K if a password is needed) |
Unquoted -a arguments | The shell splits the arguments, wrong targets | Always quote fully: -a "name=nginx state=present" |
Expecting idempotency from command/shell | The command runs every time without checking state | Use stateful modules (apt, file, systemd_service) |
Using command when a module exists | Risk of config drift and inconsistent results | Look for a built-in module first: ansible-doc -l | grep ... |
Tip
Not sure what modules exist and what their parameters are? Run ansible-doc <nama_modul> on the control node — full documentation with examples appears right in the terminal.
In episode 4, we learned that ad-hoc commands are Ansible's pocket knife for daily operations. You now know the ansible <pattern> -m <module> -a "<args>" syntax, mastered favorite modules like ping, setup, file, copy, apt/dnf, systemd_service, and understand when to use command, shell, or raw. We also practiced real scenarios — checking uptime, rolling reboot, and emergency SSH key push — plus --check mode as a safety belt.
However, there's one fundamental limitation of ad-hoc: it is undocumented and not automatically idempotent. The command you ran at 2 AM can't easily be replayed by another teammate.
In episode 5, we will overcome this limitation by learning Writing Your First Playbook & Playbook Structure — turning ad-hoc commands into YAML files that can be run repeatedly, documented, and produce readable OK, CHANGED, and FAILED statuses that anyone can read. Keep your enthusiasm up!