Chapter 4: Playbooks — The Core
Topic 22

Tasks and Module Invocation

PlaybooksModules

A task is the atomic unit of work in a play: a name:, a module, and the module's arguments. The module does the actual work — ansible.builtin.apt installs a package, ansible.builtin.template renders a config file — and the name: is the label that shows up in run output. An unnamed task costs you legibility you will miss the moment something fails in a long run.

Tasks are where a playbook stops describing structure and starts doing work. This topic pins down the three parts of a task, why the fully-qualified module name is the form to write from day one, what the name: discipline buys, and how to read a module's changed-versus-ok result as the real signal of whether anything happened.

The Three Parts of a Task

Every task is three things: a name: that labels it for humans, a module that names the action, and the module's arguments. The canonical shape is a - name: Install nginx line followed by the module key and its inputs indented beneath it. The module key plus its arguments is where the work lives; the name is purely for the person reading the output.

Two tasks from the Larkspur web play — name, module, arguments
tasks:
  - name: Install nginx
    ansible.builtin.apt:
      name: nginx
      state: present

  - name: Render larkspur.conf
    ansible.builtin.template:
      src: larkspur.conf.j2
      dest: /etc/nginx/sites-available/larkspur.conf

Read each task as those three parts. The first labels itself "Install nginx," calls ansible.builtin.apt, and passes it a package name and desired state. The second renders a template to a path. Strip the names and the modules still run identically — but the output becomes far harder to read.

FQCN vs Short Names

The fully-qualified collection name, like ansible.builtin.apt, names the collection and the module explicitly. The short name apt still resolves for builtin modules, so a beginner's playbook works without the prefix. But once your project installs collections, a community module can carry the same short name as a builtin and shadow it — and you run a different module than you meant, with no warning.

Writing the FQCN from day one removes that ambiguity for good. ansible.builtin.apt and community.postgresql.postgresql_db each name exactly one module, and a collection upgrade can never silently swap which one runs. It is a few extra characters per task that buys you a guarantee about what executes.

The name: Discipline

Give every task a descriptive name: that states intent — "Install nginx," "Render larkspur.conf," "Create the larkspur database" — because that exact string is what ansible-playbook prints as each task runs. It is also what you grep the log for when a run fails at task thirty of forty. The name is the handle you reach for every time you read the output, which is often.

A good name describes what the task is trying to achieve, not which module it calls. "Render larkspur.conf" tells you the intent at a glance; the reader does not need the module name repeated, because the output already shows it. The intent is the part only you can supply, and it is the part that makes a forty-task run readable.

What Unnamed Tasks Cost You

An unnamed task prints as TASK [ansible.builtin.template] with no hint which file or which host it touched. In a run with four template tasks, you see the same line four times and cannot tell them apart. When one of them fails, finding which file it was writing becomes a guessing game against the playbook source — at exactly the moment you least want to be guessing.

The name is free legibility you pay for once and read many times. Writing "Render larkspur.conf" takes a second when you author the task and saves minutes every time you read a run that includes it. Leaving it off saves nothing and costs you the one signal that makes failures fast to locate.

Module Arguments and Return Values

Arguments are the module's inputs — name, state, src, dest — and a module rejects any argument it does not accept. That strictness is a feature: a typo'd stat: where you meant state: stops the run with a clear error instead of silently doing the wrong thing. The module's argument list is a contract, and breaking it fails loudly.

Modules also return structured data: changed, failed, and module-specific keys. You can capture that return with register: and branch on it in a later task, which is the bridge to the control-flow chapter. The most important return to read now is changed versus ok: ok means the module checked and state already matched, while changed means it actually acted. Conflating the two misreads what a run did.

Common Mistakes
  • Leaving tasks unnamed and then reading TASK [ansible.builtin.copy] four times in the output with no way to tell which file each one wrote.
  • Using bare short module names like apt or service in a project that also installs collections, where a community module can shadow the builtin and you run the wrong one silently.
  • Writing a name: that restates the module — name: apt — instead of the intent — name: Install nginx — wasting the one field that makes output readable.
  • Passing a module an argument it does not accept and reading the hard failure as a bug — modules reject unknown args, so a typo'd stat: instead of state: stops the run by design.
  • Assuming a task that printed ok did something — ok means state already matched; only changed means the module acted, and conflating them misreads the run.
Best Practices
  • Write a clear, intent-stating name: on every task, since that string is your primary signal in run output and the thing you grep logs for.
  • Use FQCN — ansible.builtin.apt, community.postgresql.postgresql_db — everywhere from the start, so module resolution is unambiguous and collection upgrades never silently change which module runs.
  • Let the module do the work — prefer ansible.builtin.apt, template, and service over command and shell — so idempotency and changed reporting stay honest.
  • Read the changed-versus-ok distinction in the recap as the real signal of whether a run did anything, not just the presence of green text.
  • Name a task by its goal, not its module, so the output reads as a list of intents rather than a list of module calls.
Comparable tools Puppet resource a typed resource with attributes, the closest analog to a task Chef resource the same role expressed in Ruby Shell script line the unstructured equivalent with no changed reporting register captures a module's return for later control flow

Knowledge Check

What are the three parts of an Ansible task?

  • A name: that labels it, the module that performs the action, and the module's arguments
  • A host pattern that selects targets, a tag for filtering, and a handler to notify on change
  • A variable declaration, a when: condition, and a loop over an item list
  • The enclosing play, the inventory group it targets, and the exit code it returns

Why is the FQCN ansible.builtin.apt the form to write once a project installs collections?

  • It names exactly one module, so a community module sharing the short name apt can never silently shadow the builtin you meant
  • Short names like apt stop resolving entirely the moment any extra collection is installed, so every task that uses the bare module name errors out at parse time until you rewrite it as a fully qualified name
  • The fully qualified form runs faster, because Ansible skips the module-resolution lookup step
  • Only modules written as an FQCN can be given a name: label that shows in the output

What does an unnamed task cost you in a forty-task run?

  • The output labels the line by module only, so identical calls blur together and a failure leaves you guessing which file or host it touched
  • The task is skipped entirely during the run and reported as skipped in the recap, because Ansible refuses to execute any task that has no name: label and treats the missing name as an explicit opt-out
  • The task loses its idempotency and reports changed on every single run, even when nothing was altered
  • The whole run fails with a YAML syntax error before any task in the play executes

A task reports ok rather than changed. What does that mean?

  • The module checked the host and current state already matched, so it did nothing — only changed means it acted
  • The module successfully modified the host's state, which is exactly the outcome that ok reports
  • The task was skipped by a when: condition that evaluated to false on this host, so the module never ran and Ansible logged the result as ok
  • The module hit an error, but Ansible suppressed it and let the run continue

You got correct