Ansible commands blur together after a while. You know the module exists, you just can’t remember the exact flag or the right ansible-playbook option to dry-run only tagged tasks. This cheat sheet is the page you keep open in a browser tab while working.
It covers everything from quick ad-hoc commands through playbook execution flags, Jinja2 filters, lookup plugins, vault operations, async patterns, and performance tuning. Every command and output below was captured on real systems. Bookmark it, Ctrl+F when you need something, and get back to work. For a full walkthrough of setting up Ansible from scratch, see our Ansible installation guide.
Tested April 2026 on Rocky Linux 10.1, Ubuntu 24.04.4 LTS with ansible-core 2.16.14, community.general 12.5.0
Essential CLI Commands
These are the binaries you actually use day to day. If you only memorize one table from this page, make it this one.
| Command | Purpose | Common Flags |
|---|---|---|
ansible | Run ad-hoc commands against hosts | -m (module), -a (args), -i (inventory), -b (become) |
ansible-playbook | Execute playbooks | --check, --diff, --limit, --tags, -f (forks) |
ansible-galaxy | Install roles and collections | install, init, list, search |
ansible-vault | Encrypt/decrypt sensitive data | encrypt, decrypt, view, edit, rekey |
ansible-inventory | Inspect and validate inventory | --graph, --list, --host |
ansible-doc | Module documentation offline | -l (list all), -t (type: module, lookup, callback) |
ansible-config | View/dump configuration | list, dump, view |
Quick version check to confirm your install:
ansible --version
List every installed collection and its version:
ansible-galaxy collection list
Pull up offline docs for any module without leaving the terminal:
ansible-doc ansible.builtin.copy
Ad-Hoc Command Patterns
Ad-hoc commands are the fastest way to do something across your fleet without writing a playbook. The syntax is always ansible <pattern> -m <module> -a <args>. Here are the modules you’ll reach for most often. For inventory setup and host grouping, see the inventory management guide.
| Module | Example | When to Use |
|---|---|---|
ping | ansible all -m ping | Connectivity check (SSH + Python) |
shell | ansible all -m shell -a "uptime" | Run arbitrary shell commands |
command | ansible all -m command -a "df -h" | Run commands without shell features (safer) |
copy | ansible all -m copy -a "src=./f.txt dest=/tmp/f.txt" | Push files to remote hosts |
file | ansible all -m file -a "path=/tmp/f.txt state=absent" | Create, delete, or set permissions on files |
service | ansible all -m service -a "name=nginx state=started" -b | Start, stop, restart services |
package | ansible all -m package -a "name=htop state=present" -b | Install packages (distro-agnostic) |
setup | ansible all -m setup -a "filter=ansible_memory_mb" | Gather system facts |
user | ansible all -m user -a "name=deploy state=present" -b | Manage user accounts |
Check uptime across all managed hosts:
ansible all -m shell -a "uptime"
The output shows each host name followed by its uptime and load average:
managed-rocky | CHANGED | rc=0 >>
13:23:40 up 10 min, 2 users, load average: 0.34, 0.35, 0.16
managed-ubuntu | CHANGED | rc=0 >>
10:23:40 up 9 min, 1 user, load average: 0.06, 0.10, 0.04
Gather memory facts from a specific host group:
ansible rocky_nodes -m setup -a "filter=ansible_memory_mb"
This returns structured JSON you can parse or feed into templates:
managed-rocky | SUCCESS => {
"ansible_facts": {
"ansible_memory_mb": {
"real": { "free": 2727, "total": 3653, "used": 926 }
}
}
}
Push a file to all hosts, then clean it up:
ansible all -m copy -a "content='hello from ansible\n' dest=/tmp/ansible-test.txt mode=0644"
Confirm the file landed:
managed-rocky | CHANGED => { "changed": true, "dest": "/tmp/ansible-test.txt", "mode": "0644" }
Remove it when you’re done:
ansible all -m file -a "path=/tmp/ansible-test.txt state=absent"
The state changes to absent:
managed-rocky | CHANGED => { "changed": true, "path": "/tmp/ansible-test.txt", "state": "absent" }
For more on managing users and groups with ad-hoc commands and playbooks, see managing Linux users with Ansible.
Inventory Quick Reference
A well-organized inventory is the foundation. Here’s a minimal INI-style inventory with two groups:
[rocky_nodes]
managed-rocky ansible_host=10.0.1.11
[ubuntu_nodes]
managed-ubuntu ansible_host=10.0.1.12
[all:vars]
ansible_user=rocky
ansible_python_interpreter=/usr/bin/python3
Visualize the group hierarchy with --graph:
ansible-inventory -i inventory.ini --graph
This draws a tree of all groups and their members:
@all:
|--@ungrouped:
|--@rocky_nodes:
| |--managed-rocky
|--@ubuntu_nodes:
| |--managed-ubuntu
Dump the full inventory as JSON (useful for debugging dynamic inventories):
ansible-inventory -i inventory.ini --list
Playbook Execution Flags
Knowing these flags saves you from accidentally deploying to production when you meant to do a dry run. The playbook tutorial covers writing playbooks from scratch; this section focuses on execution control.
| Flag | What It Does | Example |
|---|---|---|
--check | Dry run, no changes applied | ansible-playbook site.yml --check |
--diff | Show file change diffs (combine with --check) | ansible-playbook site.yml --check --diff |
--syntax-check | Validate YAML syntax without running | ansible-playbook site.yml --syntax-check |
--list-tasks | Show all tasks without executing | ansible-playbook site.yml --list-tasks |
--list-hosts | Show targeted hosts without executing | ansible-playbook site.yml --list-hosts |
--limit | Restrict execution to specific hosts/groups | ansible-playbook site.yml --limit rocky_nodes |
--tags | Run only tasks with matching tags | ansible-playbook site.yml --tags "install,config" |
--skip-tags | Skip tasks with matching tags | ansible-playbook site.yml --skip-tags "debug" |
--start-at-task | Resume from a specific task name | ansible-playbook site.yml --start-at-task "Configure Nginx" |
-f / --forks | Number of parallel processes | ansible-playbook site.yml -f 20 |
-e / --extra-vars | Pass variables at runtime | ansible-playbook site.yml -e "env=staging" |
--step | Interactive: confirm each task before running | ansible-playbook site.yml --step |
Preview which tasks a playbook will execute:
ansible-playbook deploy-nginx.yml --list-tasks
The output lists every task name in execution order:
playbook: deploy-nginx.yml
play #1 (all): Deploy Nginx on Rocky Linux and Ubuntu TAGS: []
tasks:
Install firewall Python bindings (RHEL family) TAGS: []
Install Nginx (RHEL family) TAGS: []
Install Nginx (Debian family) TAGS: []
...
See which hosts would be targeted:
ansible-playbook deploy-nginx.yml --list-hosts
Confirms the host pattern before you commit:
play #1 (all): Deploy Nginx on Rocky Linux and Ubuntu TAGS: []
pattern: ['all']
hosts (2):
managed-rocky
managed-ubuntu
A quick syntax validation catches YAML errors before anything touches a remote host:
ansible-playbook deploy-nginx.yml --syntax-check
Clean output means no errors found:
playbook: deploy-nginx.yml
Verbosity Levels
When something fails and the error message is useless, bump the verbosity. Each level adds more detail.
| Flag | Level | What It Shows |
|---|---|---|
-v | 1 | Task results and changed status |
-vv | 2 | Input parameters for each task |
-vvv | 3 | SSH connection details, file transfers, command execution |
-vvvv | 4 | Connection plugin internals (useful for SSH debugging) |
For most debugging, -vvv is the sweet spot. Going to -vvvv floods the terminal but helps diagnose SSH key or connection issues.
ansible-playbook site.yml -vvv
Variables, Facts, and Loops
Variables are where playbooks get their flexibility. This section covers the patterns you’ll use constantly.
Register output from a command and use it later:
- name: Check disk space
command: df -h /
register: disk_output
- name: Show disk info
debug:
msg: "{{ disk_output.stdout_lines[0] }}"
Set a fact dynamically during execution:
- name: Set database port based on OS
set_fact:
db_port: "{{ 5432 if ansible_os_family == 'Debian' else 5433 }}"
Loop over a list of packages:
- name: Install monitoring tools
package:
name: "{{ item }}"
state: present
loop:
- htop
- iotop
- strace
- tcpdump
The older with_items syntax still works but loop is the modern replacement. Use loop for new playbooks.
Conditional execution with when:
- name: Install EPEL on RHEL family
dnf:
name: epel-release
state: present
when: ansible_os_family == "RedHat"
- name: Enable UFW on Ubuntu
ufw:
state: enabled
when: ansible_distribution == "Ubuntu"
Loop with index:
- name: Create numbered config files
copy:
content: "server_id={{ idx }}"
dest: "/etc/app/node-{{ idx }}.conf"
loop: "{{ groups['all'] }}"
loop_control:
index_var: idx
For database automation examples using variables and loops, check managing PostgreSQL with Ansible.
Error Handling: block/rescue/always
The block/rescue/always pattern is Ansible’s try/catch/finally. Wrap risky tasks in a block, handle failures in rescue, and run cleanup in always. This catches most people off guard because they don’t realize Ansible has structured error handling at all.
- name: Error handling demo
hosts: rocky_nodes
tasks:
- block:
- name: Try to start a non-existent service
service:
name: nonexistent-service
state: started
rescue:
- name: Handle the failure
debug:
msg: "Service failed to start. Running recovery steps."
- name: Log the failure
shell: echo "Service failure at $(date)" >> /tmp/ansible-errors.log
always:
- name: This always runs regardless of success or failure
debug:
msg: "Cleanup complete. Block finished."
Here’s what that looks like in practice:
TASK [Try to start a non-existent service] *************************************
fatal: [managed-rocky]: FAILED! => {"changed": false, "msg": "Could not find the requested service nonexistent-service: host"}
TASK [Handle the failure] ******************************************************
ok: [managed-rocky] => {
"msg": "Service failed to start. Running recovery steps."
}
TASK [Log the failure] *********************************************************
changed: [managed-rocky]
TASK [This always runs regardless of success or failure] ***********************
ok: [managed-rocky] => {
"msg": "Cleanup complete. Block finished."
}
PLAY RECAP *********************************************************************
managed-rocky : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
Notice rescued=1 in the play recap. That’s your signal the rescue block fired.
Other error control directives:
| Directive | Purpose | Example |
|---|---|---|
ignore_errors: true | Continue even if task fails | Non-critical cleanup tasks |
failed_when | Custom failure condition | failed_when: "'ERROR' in result.stderr" |
changed_when | Custom change condition | changed_when: "'created' in result.stdout" |
any_errors_fatal: true | Stop all hosts if one fails | Database migrations (must succeed everywhere) |
Jinja2 Filters Quick Reference
Filters transform data inside {{ }} expressions. Ansible ships with dozens; these are the ones that come up repeatedly in real playbooks. All outputs below are from actual runs on Rocky Linux 10.1.
| Filter | Input | Output |
|---|---|---|
unique | ["nginx","redis","nginx","postgresql","redis"] | ["nginx", "redis", "postgresql"] |
from_json | '{"name":"web01","port":8080}' | Server web01 on port 8080 |
default | undefined_var | default("production") | Environment is production |
ternary | (ansible_os_family == "RedHat") | ternary("RHEL-based","Debian-based") | OS family is RHEL-based |
sort + join | [3,1,4,1,5,9,2,6] | sort | join(", ") | 1, 1, 2, 3, 4, 5, 6, 9 |
regex_replace | "Hello World 1234" | regex_replace("\d+","XXXX") | Hello World XXXX |
combine | {"a":1,"b":2} | combine({"b":3,"c":4}) | {"a": 1, "b": 3, "c": 4} |
selectattr | List of dicts filtered by active == true | Active users: ['alice', 'carol'] |
Here’s the selectattr filter in action, which is particularly useful for filtering lists of dictionaries:
- name: selectattr filter
vars:
users:
- { name: alice, active: true }
- { name: bob, active: false }
- { name: carol, active: true }
debug:
msg: "Active users: {{ users | selectattr('active') | map(attribute='name') | list }}"
Result on managed-rocky:
ok: [managed-rocky] => { "msg": "Active users: ['alice', 'carol']" }
The full list of built-in filters is in the official Ansible filters documentation.
Lookup Plugins
Lookups pull data from external sources into your playbooks. They run on the control node, not the remote hosts.
| Lookup | Purpose | Example Usage |
|---|---|---|
file | Read local file contents | {{ lookup('file', '/etc/hostname') }} |
env | Read environment variables | {{ lookup('env', 'HOME') }} |
sequence | Generate number sequences | {{ query('sequence', 'start=8080 end=8085') }} |
dict | Iterate over dictionaries | {{ lookup('dict', port_map) }} |
pipe | Run a command and return stdout | {{ lookup('pipe', 'date +%Y%m%d') }} |
password | Generate or retrieve passwords | {{ lookup('password', '/tmp/pw length=16') }} |
template | Render a Jinja2 template | {{ lookup('template', 'my.conf.j2') }} |
Verified outputs from the test environment:
TASK [file lookup] *************************************************************
ok: [managed-rocky] => { "msg": "ansible.cfg contents (first line): [defaults]" }
TASK [env lookup] **************************************************************
ok: [managed-rocky] => { "msg": "Home directory: /home/rocky" }
TASK [sequence lookup] *********************************************************
ok: [managed-rocky] => { "msg": "Ports: 8080, 8081, 8082, 8083, 8084, 8085" }
TASK [dict lookup] *************************************************************
ok: [managed-rocky] => (item=http) => { "msg": "http=80" }
ok: [managed-rocky] => (item=https) => { "msg": "https=443" }
ok: [managed-rocky] => (item=ssh) => { "msg": "ssh=22" }
The dict lookup is especially handy for iterating over port maps, service configurations, or any key/value structure without converting it to a list first.
Vault Operations
Ansible Vault encrypts sensitive data (passwords, API keys, certificates) so you can safely commit them to version control. Our dedicated Ansible Vault reference guide covers advanced patterns; here’s the quick reference.
| Operation | Command |
|---|---|
| Encrypt a file | ansible-vault encrypt secrets.yml |
| Decrypt a file | ansible-vault decrypt secrets.yml |
| View encrypted file | ansible-vault view secrets.yml |
| Edit in-place | ansible-vault edit secrets.yml |
| Change password | ansible-vault rekey secrets.yml |
| Encrypt a single string | ansible-vault encrypt_string 'secret' --name 'db_pass' |
| Run playbook with vault | ansible-playbook site.yml --ask-vault-pass |
| Use password file | ansible-playbook site.yml --vault-password-file .vault_pass |
Encrypt a secrets file:
ansible-vault encrypt secrets.yml
Output on success:
Encryption successful
The file now contains ciphertext instead of plaintext:
$ANSIBLE_VAULT;1.1;AES256
30353634656539386438653836393166...
View the contents without decrypting the file on disk:
ansible-vault view secrets.yml
Shows the original plaintext:
db_password: SuperSecret123
Encrypt a single variable inline for use in playbooks:
ansible-vault encrypt_string 'SuperSecret123' --name 'db_password'
Paste the output directly into your vars file. The playbook decrypts it at runtime when you pass --ask-vault-pass or --vault-password-file.
Async Tasks
Long-running tasks (backups, large package upgrades, database migrations) can block your playbook. The async pattern fires off the task, does other work, then checks back.
- name: Run a long task asynchronously
shell: sleep 15 && echo "done"
async: 60
poll: 0
register: async_task
- name: Do other work while waiting
debug:
msg: "Doing other work while async task runs..."
- name: Check async task status
async_status:
jid: "{{ async_task.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 10
delay: 5
The key settings: async sets the maximum runtime in seconds, poll: 0 means “don’t wait, move on immediately.” Then async_status polls the job ID until it completes.
Real execution output showing the retry loop:
TASK [Run a long task asynchronously] ******************************************
changed: [managed-rocky]
changed: [managed-ubuntu]
TASK [Do other work while waiting] *********************************************
ok: [managed-rocky] => { "msg": "Doing other work while async task runs..." }
TASK [Check async task status] *************************************************
FAILED - RETRYING: [managed-rocky]: Check async task status (10 retries left).
FAILED - RETRYING: [managed-ubuntu]: Check async task status (10 retries left).
changed: [managed-ubuntu]
changed: [managed-rocky]
TASK [Show async result] *******************************************************
ok: [managed-rocky] => { "msg": "Task complete on managed-rocky" }
The FAILED - RETRYING messages are normal. They mean the task hasn’t finished yet and Ansible is polling again.
Performance Tuning
Default Ansible settings are conservative. On a fleet of 50+ hosts, these tweaks make a real difference. We benchmarked forks on our two-node test setup and the improvement scales with host count.
| Setting | Default | Recommended | Impact |
|---|---|---|---|
forks | 5 | 20-50 | Parallel host execution. More forks = more hosts at once |
serial | all hosts | 25% or batch count | Rolling deploys, limits blast radius |
gather_facts | true | false (when not needed) | Saves 2-5 seconds per host on every play |
pipelining | false | true | Reduces SSH operations per task |
fact_caching | memory | jsonfile | Persist facts across runs, skip re-gathering |
strategy | linear | free | Fast hosts don’t wait for slow ones |
Add these to your ansible.cfg:
[defaults]
forks = 25
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[ssh_connection]
pipelining = True
Benchmark: forks impact on a 2-host inventory
| Forks | Execution Time | Improvement |
|---|---|---|
| 1 | 2.57s | baseline |
| 5 (default) | 1.65s | 36% faster |
With only 2 hosts, the difference is already noticeable. On 50+ hosts, moving from forks=5 to forks=25 can cut execution time by 60-70%.
Enable pipelining to reduce SSH round trips per task:
ANSIBLE_PIPELINING=true ansible-playbook site.yml
One requirement: requiretty must be disabled in /etc/sudoers on the managed hosts. Rocky Linux 10 and Ubuntu 24.04 both have it disabled by default.
Useful Playbook Patterns
A few patterns that show up in almost every production environment.
Handler pattern (restart only when config changes):
tasks:
- name: Deploy Nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
The handler fires only if the template task reports a change. No config change, no unnecessary restart.
Rolling update with serial:
- hosts: webservers
serial: 2
tasks:
- name: Pull latest code
git:
repo: https://github.com/org/app.git
dest: /var/www/app
- name: Restart application
service:
name: app
state: restarted
This deploys to 2 hosts at a time. If any batch fails, the remaining hosts stay untouched.
Delegate to a different host:
- name: Remove host from load balancer before maintenance
uri:
url: "http://10.0.1.10:8080/api/pool/remove/{{ inventory_hostname }}"
method: POST
delegate_to: 10.0.1.10
The task runs on the control node (or another host) but in the context of the current host. Handy for load balancer drain, DNS updates, or monitoring actions.
For automating SSL certificate generation across your fleet, see generating SSL certificates with Ansible. LVM and disk management playbooks are covered in LVM automation with Ansible.
Galaxy Roles and Collections
Don’t reinvent the wheel. Galaxy has community roles for nearly everything.
ansible-galaxy role install geerlingguy.docker
Install a collection (the modern replacement for standalone roles):
ansible-galaxy collection install community.general
Install all dependencies from a requirements file:
ansible-galaxy install -r requirements.yml
A typical requirements.yml:
---
collections:
- name: community.general
version: ">=12.0.0"
- name: ansible.posix
roles:
- name: geerlingguy.docker
- name: geerlingguy.nginx
For container automation after installing Docker, check managing Docker containers with Ansible.
Troubleshooting Checklist
When a playbook fails and the error is unclear, work through this list. These cover the issues we run into most often in production.
| Symptom | Check | Fix |
|---|---|---|
| SSH connection timeout | ansible all -m ping -vvvv | Verify SSH key, check firewall, confirm ansible_user |
| “No module named” error | ansible_python_interpreter in inventory | Set to /usr/bin/python3 explicitly |
| Become password required | Prompt appears mid-run | Add --ask-become-pass or configure passwordless sudo |
| Stale facts | Facts from a previous run | Delete fact_caching_connection directory |
| Variable undefined | -vvv to see variable resolution | Check variable precedence (extra vars > inventory > defaults) |
| “Vault password required” | Encrypted vars without vault pass | Add --ask-vault-pass or --vault-password-file |
| SELinux denials on RHEL | ausearch -m avc -ts recent | Add seboolean or semanage tasks to playbook |
When all else fails, run with -vvvv and read the SSH negotiation output line by line. The answer is almost always there.
The Ansible configuration reference documents every setting available in ansible.cfg if you need to dig deeper into connection tuning or plugin configuration.