Anatomy of a Task
A task is the smallest unit of work in a playbook: it has a human-readable 'name' and calls exactly one module with a set of parameters. The module — such as ansible.builtin.apt, ansible.builtin.copy, or ansible.builtin.service — does the actual work on the target host, and Ansible reports back whether that task resulted in a change ('changed'), no change needed ('ok'), or a failure ('failed').
Cricket analogy: A task is like a single ball bowled in an over: the module is the specific delivery type (yorker, googly), the parameters are line and length, and the umpire's call afterward mirrors Ansible reporting changed, ok, or failed.
Idempotency and Common Modules
Well-written Ansible modules are idempotent: running the same task repeatedly produces the same end state without side effects, and Ansible only reports 'changed' the first time the desired state is actually applied. This is why modules like ansible.builtin.apt (state: present) or ansible.builtin.file (state: directory) are preferred over the ansible.builtin.command or ansible.builtin.shell modules, which execute raw commands and have no built-in awareness of whether the system already matches the desired state.
Cricket analogy: Idempotency is like DRS reviewing the same delivery twice — the outcome (out or not out) doesn't change no matter how many times you re-check it, unlike a raw shell command that just re-bowls blindly.
tasks:
- name: Install required packages (idempotent)
ansible.builtin.apt:
name:
- git
- curl
- python3-pip
state: present
- name: Ensure application directory exists
ansible.builtin.file:
path: /opt/myapp
state: directory
owner: appuser
group: appuser
mode: '0755'
- name: Run a one-off migration script (not naturally idempotent)
ansible.builtin.command: /opt/myapp/bin/migrate.sh
args:
creates: /opt/myapp/.migrated
command vs. shell, and Finding the Right Module
ansible.builtin.command runs a program directly without invoking a shell, so it cannot use pipes, redirects, or environment variable expansion, but it is safer against shell injection. ansible.builtin.shell runs the command through /bin/sh, enabling pipes and redirection at the cost of that safety. In almost every case, checking 'ansible-doc -l' for a purpose-built module (git, mysql_db, systemd, user, cron, etc.) is better than reaching for command or shell, because purpose-built modules handle idempotency, error checking, and check-mode support for you.
Cricket analogy: Choosing command over shell is like a batter using a defensive block against a tricky delivery instead of trying a risky reverse-sweep — command avoids the 'shell injection' equivalent of getting caught off a mistimed shot.
Avoid ansible.builtin.shell and ansible.builtin.command for anything that has a dedicated module (package installs, service management, file operations, user management). Raw shell tasks are not idempotent by default, can fail silently on repeated runs, and don't support check-mode diffs the way real modules do — use 'creates' or 'changed_when' if you must use them.
- A task pairs a name with exactly one module call and its parameters.
- Modules report changed, ok, or failed after execution.
- Idempotent modules produce the same end state regardless of how many times they run.
- command runs without a shell (no pipes/redirects); shell runs via /bin/sh (supports them but less safe).
- Prefer purpose-built modules (apt, file, service, user, git, etc.) over command/shell whenever one exists.
- Use 'ansible-doc -l' and 'ansible-doc <module>' to discover and inspect available modules.
- Use 'creates' or 'changed_when' to give command/shell tasks idempotent-like behavior when unavoidable.
Practice what you learned
1. What does it mean for an Ansible module to be idempotent?
2. What is a key difference between ansible.builtin.command and ansible.builtin.shell?
3. Which tool would you use to discover what parameters the ansible.builtin.copy module accepts?
4. Why is ansible.builtin.apt with state: present generally preferred over a raw shell 'apt-get install' command?
5. What does the 'creates' argument do when used with ansible.builtin.command?
Was this page helpful?
You May Also Like
Playbook Structure Basics
Learn how Ansible playbooks are organized as YAML documents of plays and tasks, and how top-level keys like hosts, vars, and roles fit together.
Handlers and Notifications
Learn how Ansible handlers run only when notified by a change, and how to coordinate restarts and other follow-up actions efficiently.
Loops in Ansible
Use the 'loop' keyword and its variants to run a single task repeatedly over lists, dictionaries, and nested data structures.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics