Red Hat Certified Engineer (RHCE) #4 Ad-hoc commands: running modules on the spot

8 min read

In #3 we nailed down how the control node connects to managed nodes through ansible.cfg, SSH, and become. Once the connection is in place, it’s time to actually send commands. Before writing a playbook, let’s get comfortable with ad-hoc commands — running a module on the spot in a single line. Ad-hoc is the tool for quick checks and one-off tasks, and at the same time it’s the fastest way to get a feel for what each module does.

What an ad-hoc command is #

An ad-hoc command is a way of invoking a single module with one line of the ansible command, without creating a playbook file. It suits one-shot tasks like “check right now whether every web server is alive,” “install a single package on a specific group,” or “deploy one file.” If you have to repeat the same task every time or chain several tasks together, a playbook is the right fit — but for a check you’ll look at once and be done with, ad-hoc is far faster.

The basic structure of an ad-hoc command is as follows.

Basic ad-hoc structure
ansible <pattern> -m <module> -a "<arguments>"

Here’s what each element means.

ElementMeaning
<pattern>The target host pattern. A hostname, group name, or wildcard from the inventory
-m <module>The name of the module to run. If omitted, the default module (command) is used
-a "<args>"The arguments to pass to the module. The key=value form is standard
-bEscalate privileges with become (root by default)
-i <inventory>Specify the inventory file

The simplest example — sending the ping module to every host in the web group — looks like this.

Run the ping module
ansible web -m ping

Here ping is not ICMP; it’s a module that checks whether Ansible can connect to the managed node and run Python. If pong comes back, it means both the SSH connection and the Python environment are healthy.

Host patterns #

Whether ad-hoc or playbook, the first argument is always the target host pattern. The exam often demands things like “apply this only to this group,” so let’s learn the pattern syntax precisely.

PatternTarget
all or *Every host in the inventory
webHosts belonging to the web group
web:dbThe web or db group (union)
web:&dbHosts in web that are also in db (intersection)
web:!stagingHosts in web but excluding staging (difference)
192.168.*Hostnames/IPs matching the pattern
web[0:2]Selecting by index range within a group

For example, to send a command only to hosts in the web group minus staging, write it like this. We wrap it in single quotes so the shell doesn’t interpret !.

Run with an exclusion pattern
ansible 'web:!staging' -m ping

To check ahead of time which hosts the target actually resolves to, append --list-hosts.

List target hosts
ansible 'web:!staging' --list-hosts

Frequently used modules #

Let’s go over the modules used most often with ad-hoc. You use the same modules in playbooks unchanged, so learning the options here makes #5 onward much smoother.

ping #

Checks connectivity and Python execution. It takes no arguments.

Check connectivity
ansible all -m ping

The difference between command and shell #

Both run a command on the managed node, but they behave differently.

  • command. The default module. It runs the command directly without going through a shell. As a result, you can’t use shell features like pipes (|), redirects (>), or variable expansion ($HOME). It’s more secure, so it’s preferred when you don’t need shell features.
  • shell. It runs through /bin/sh, so pipes, redirects, and environment variable expansion all work.
command vs shell
# command: runs directly, without shell features
ansible web -m command -a "uptime"

# command is the default module, so -m can be omitted
ansible web -a "id"

# shell: when you need pipes and redirects
ansible web -m shell -a "ps aux | grep nginx"

If you use a pipe with command, the | gets passed as an argument to the command and it fails, so use shell only in those cases.

copy #

Copies a file from the control node to the managed node, or writes content directly.

copy module
# copy a file
ansible web -m copy -a "src=/etc/motd dest=/etc/motd owner=root mode=0644" -b

# write content directly with content
ansible web -m copy -a 'content="Managed by Ansible\n" dest=/etc/motd' -b

file #

Manages the state of files and directories (existence, permissions, owner, links).

file module
# create a directory
ansible web -m file -a "path=/opt/app state=directory owner=app mode=0755" -b

# delete a file
ansible web -m file -a "path=/tmp/old.log state=absent" -b

# create a symbolic link
ansible web -m file -a "src=/opt/app/current dest=/opt/app/live state=link" -b

The main values of state are directory (directory), touch (empty file), absent (delete), and link (symlink).

package and dnf #

Installs or removes packages. dnf is RHEL-family only, while package is a generic module that automatically picks the package manager matching the distribution.

dnf and package modules
# install with dnf
ansible web -m dnf -a "name=httpd state=present" -b

# install with package (generic)
ansible web -m package -a "name=httpd state=present" -b

# update to the latest
ansible web -m dnf -a "name=httpd state=latest" -b

# remove
ansible web -m dnf -a "name=httpd state=absent" -b

service and systemd #

Starts or stops a service, or sets it to start automatically at boot.

service and systemd modules
# start and enable automatic start at boot
ansible web -m service -a "name=httpd state=started enabled=yes" -b

# restart with the systemd module
ansible web -m systemd -a "name=httpd state=restarted" -b

state accepts started, stopped, restarted, and reloaded, and enabled decides whether it starts automatically at boot.

user #

Creates or removes user accounts.

user module
# create a user
ansible web -m user -a "name=deploy groups=wheel state=present" -b

# remove a user (including the home directory)
ansible web -m user -a "name=deploy state=absent remove=yes" -b

lineinfile #

Ensures or replaces a specific line within a file. It’s handy when you need to change just one line of a config file.

lineinfile module
ansible web -m lineinfile -a 'path=/etc/ssh/sshd_config regexp="^#?PermitRootLogin" line="PermitRootLogin no"' -b

If a line matching regexp exists, it’s replaced with line; if not, the line is appended to the end of the file.

Escalating privileges with become #

For tasks that need root privileges, such as installing packages or modifying system files, append -b (or --become) to escalate. To escalate to a specific user, use --become-user alongside it.

Escalate with become
# escalate to root and install a package
ansible web -m dnf -a "name=vim state=present" -b

# escalate to a specific user
ansible web -m command -a "whoami" -b --become-user=app

If you set become=true in ansible.cfg back in #3, escalation happens even without -b — but in ad-hoc, explicitly adding -b makes the intent clearer.

Looking up module options with ansible-doc #

There’s no internet in the exam room, so you look up a module’s options with ansible-doc. The habit of quickly scanning the module name, its main options, and the EXAMPLES at the bottom of the doc saves time.

Using ansible-doc
# full module documentation
ansible-doc copy

# one-line summary only
ansible-doc -s copy

# list of installed modules
ansible-doc -l

ansible-doc -s <module> shows the options in a key=value skeleton, which is great for quickly confirming how to write the arguments. When you can’t recall a module name, narrow down the candidates with ansible-doc -l | grep <keyword>.

Exam points #

  • command vs shell. If you need pipes, redirects, or variable expansion, use shell; otherwise use command (the default module). Using shell when you don’t need shell features needlessly raises the risk.
  • Watch out for non-idempotent modules. command and shell aren’t idempotent, so they’re marked as changed on every run. When you must guarantee the same result, using state-based modules like copy, file, dnf, and service matches the RHCE grading criteria.
  • Be explicit about become. For tasks that need privileges, leaving out -b fails with a permission error, so add it as a habit.
  • ansible-doc is the only reference. When an option name is fuzzy, don’t guess — confirm it with ansible-doc -s.
  • Lean toward playbooks over ad-hoc. The exam mostly demands answers as playbooks, but ad-hoc is useful for connectivity checks and verifying module behavior, so get both approaches into your hands.

The boundary between ad-hoc and playbook #

Ad-hoc is fast and lightweight, but it hits its limits when you need to chain two or more tasks in order or use variables, handlers, and conditionals. If you need to run several tasks in sequence, configure hosts differently per host with variables and Jinja2 templates, use a handler that restarts a service only when a change occurs, or repeat and version-control the same task, it’s time to move on to a playbook. In other words, ad-hoc is “check or run once, right now,” and configuration that has to be repeated and reproduced is the playbook’s job.

Wrap-up #

What this post locked in:

  • Ad-hoc structure. Run a single module on the spot with ansible <pattern> -m <module> -a "args".
  • Host patterns. Pick targets with all, group names, : (union), :& (intersection), :! (difference), and * (wildcard).
  • Frequently used modules. We practiced ping, command/shell, copy, file, package/dnf, service/systemd, user, and lineinfile with examples.
  • become. Add -b to tasks that need privileges.
  • command vs shell. Use shell only when you need shell features, and remember that command and shell aren’t idempotent.
  • ansible-doc. In an environment with no internet, ansible-doc -s <module> is the only reference.

Next — Playbook basics #

We’ve nailed down how to run a single module on the spot with ad-hoc. Now comes the step of bundling those same modules into a file to make them repeatable.

In #5 Playbook basics: task, handler, idempotency, we’ll work through the basic structure of a playbook (play, task, module), the handler that runs only when a change occurs, and idempotency — the core of RHCE grading — writing it by hand and verifying it by running twice.

X