Playbooks, Tasks & Modules In Depth
Ansible
Chapter 4 · Playbooks, Tasks & Modules In Depth
ansible1-2 showed a minimal playbook without explaining what actually makes it safe to run more than once. This chapter is where that gets explained properly — modules as the real unit of work, and idempotency, the principle everything else in this course assumes.
Modules — The Atomic Unit of Work
Every task calls exactly one module — a small, self-contained unit of code Ansible pushes to the target, executes, and receives a structured result back from. ansible1-1's own "batteries included" claim is concrete here: modules exist for package management (apt, yum, dnf), files (copy, template, file), services (service, systemd), users, and practically every major cloud provider's own API. Critically, a module describes desired state — state: present, state: started — not a literal command to execute. That single design choice is the foundation idempotency is built on.
Idempotency — The Central Design Principle
An idempotent operation produces the same end result no matter how many times it's applied. Run a playbook once, and the system converges toward the desired state. Run the exact same playbook again immediately afterward, and nothing should change — every task reports ok, not changed, because the state it describes already exists. Contrast this with a naive imperative script doing echo "some line" >> config_file — run twice, and the line appears twice. Modules like lineinfile/blockinfile exist specifically to make this kind of edit idempotent: they check the file's current content first, and only make a change if the desired line genuinely isn't already there.
Ansible's Idempotency vs. Kubernetes' Continuous Reconciliation
ansible1-1 promised this contrast directly. k8s1-2's own reconciliation loop continuously observes actual state and corrects drift automatically, all the time, without anyone triggering it. Ansible's idempotency is a genuinely different kind of guarantee: a playbook, when run, converges the target toward the desired state — but nothing happens between runs. If something drifts an hour after a playbook finishes, Ansible does nothing about it until that playbook is explicitly run again. Idempotent design makes repeated runs safe and convergent; it does not provide continuous enforcement the way Kubernetes' reconciliation loop does. Neither is universally better — Kubernetes needs continuous self-healing for a live running system; Ansible is typically run on a schedule or on demand, for configuration management specifically.
Task Structure In Depth
name is a human-readable label shown in the run output — always name every task, so a real run's output actually tells you which step did what. loop runs the task once per item in a list, with {{ item }} substituting the current value each time. Other common task-level keywords: when (conditionally skip a task), register (capture a task's own result into a variable for a later task to use), and tags (run only a selected subset of tasks on a given invocation).
A Non-Idempotent Trap — the command/shell Modules
command and shell run arbitrary commands directly — a genuinely useful escape hatch when no dedicated module exists for a specific task. They are not idempotent by default, though: a shell command that unconditionally does something (appends to a file, restarts a service every time) reports changed on every single run, even when nothing meaningful actually needed to change. The fix is explicit: the creates/removes arguments tell Ansible to only run the command if a given file doesn't (or does) already exist, and changed_when: false lets a task explicitly declare its own changed status, restoring honest, idempotent-style reporting even while using this lower-level tool.
| Idempotent by default? | What it describes | |
|---|---|---|
| apt / service / file / ... | Yes | Desired state — Ansible figures out whether action is needed |
| command / shell | No — reports changed every run unless told otherwise | A literal command to execute, every time |
ansible-playbook --check runs a playbook in "dry run" mode — every task reports what it would do, without actually doing it. Genuinely useful for validating a change before it runs for real, especially given how much of this site's own general guidance leans toward reviewing risky operations before committing to them.
command/shell task, check whether a dedicated module already covers it — ansible1-2's own ansible-doc -l lists every built-in module. A dedicated module gives you idempotency (and usually better portability across operating systems) for free; command/shell should be reserved for the genuine gaps, not used out of habit.
Hands-On Exercises
Explain, using the echo/config-file example from this chapter, exactly why running a non-idempotent task twice produces a different result than running an idempotent module twice.
📄 View solutionA colleague asks why Ansible doesn't just run continuously in the background the way Kubernetes does, to catch configuration drift immediately instead of waiting for the next playbook run. Explain the real architectural reason, referencing this chapter's own comparison.
📄 View solutionA task uses shell: "systemctl restart myapp" with no changed_when or creates argument. Explain what problem this causes when the playbook is run repeatedly, and rewrite it (in words or YAML) so it only reports changed when the restart genuinely happened.
📄 View solutionChapter 4 Quick Reference
- A module is the atomic unit of work — it describes desired state, not a literal command
- Idempotency — running the same playbook twice makes no further changes the second time; the foundation this course is built on
- Ansible's idempotency (repeated runs converge safely) is different from Kubernetes' own continuous reconciliation loop (constant, automatic drift correction) — different guarantees for different problems
- loop (iterate), register (capture output), when (conditional), tags (selective execution) — common task-level keywords
- command/shell are not idempotent by default — use
creates/removesorchanged_whento fix that - ansible-playbook --check — dry-run mode, shows what would change without changing anything
- Reach for a dedicated module before command/shell — idempotency and portability come free with the former