Chapter 7: Control Flow
Topic 43

Tags

Workflow

Tags label tasks, blocks, and roles so you can run or skip parts of a large playbook without splitting it into separate files. --tags deploy runs only the tagged tasks; --skip-tags config runs everything except them — the difference between re-running a full 60-task playbook and touching just the release swap.

Two special tags, always and never, override the selection entirely. The one thing tags do not do is reorder anything — they select which tasks run, never the sequence, and treating them as a dependency mechanism is the fastest way to ship a half-applied host.

Tagging Tasks, Blocks, and Roles

tags: [deploy] on a task marks that one task. A tag on a block applies to every task inside it, so you tag a logical group once. A tag on a role — or on a role entry under roles: — applies to the whole role, which is how you run "just the config role" of a multi-role play without naming every task inside it. The same tag can sit on tasks across different files, and --tags gathers all of them.

--tags and --skip-tags

--tags runs only the matching tasks and skips everything else; --skip-tags runs all but the matching ones. Passing neither runs the whole playbook as normal. The two compose for fine selection — --tags deploy --skip-tags slow runs the deploy tasks except the slow ones. The mental model is a filter over the flat task list, applied before execution begins.

always and never

A task tagged always runs on every invocation regardless of --tags — the right home for fact gathering or a safety precheck that later tasks depend on. A task tagged never runs only when you explicitly name it with --tags, which is how you hide a destructive or rarely-needed task behind an opt-in so a normal run never touches it. These two override the ordinary selection: always ignores filters, never requires being named.

Carving Larkspur into Phases

Tag the package-install and template tasks config, and the release-swap and restart tasks deploy. Now --tags deploy ships a new build without re-running the whole config pass, and --tags config pushes a settings change without triggering a deploy. One playbook, two operations, selected at the command line — instead of two playbooks that drift apart.

Tag tasks by phase so one playbook serves config and deploy
- name: Install the web-tier packages
  ansible.builtin.apt: { name: "{{ web_packages }}", state: present }
  tags: [config]

- name: Gather facts the deploy step relies on
  ansible.builtin.setup:
  tags: [always]

- name: Swap to the new release and restart gunicorn
  ansible.builtin.command: /usr/local/bin/larkspur-release {{ new_release }}
  tags: [deploy]

The fact-gathering task carries always because both a --tags config run and a --tags deploy run need those facts; tag it config instead and a deploy-only run would reference facts that were never gathered.

Listing and Verifying

--list-tags shows every tag defined in the play, and --list-tasks shows which tasks a given invocation would actually run — the check that confirms a --tags selection hits exactly what you intend before anything executes. On a 60-task playbook against production, running --list-tasks first is the cheap insurance that a tag selection does not silently skip a prerequisite or pull in something you forgot was tagged.

Common Mistakes
  • Tagging only some tasks of a logical unit, so --tags deploy swaps the release but skips the untagged handler that restarts it, leaving old code running under a new symlink.
  • Forgetting that a task with no tag is skipped under --tags, so a --tags config run silently omits an untagged prerequisite and the config applies against a half-set-up host.
  • Tagging fact gathering or a guard with a normal tag instead of always, so a --tags deploy run skips it and later tasks reference facts that were never gathered.
  • Relying on tags to enforce ordering — tags select which tasks run, not the order; an untagged dependency that --tags skips will not magically run first.
  • Leaving a destructive task on a normal tag instead of never, so a broad --tags selection sweeps it in and runs the dangerous step nobody meant to trigger.
Best Practices
  • Tag complete logical units — every task plus its handler — so a --tags deploy run is self-contained and never half a phase.
  • Tag fact gathering and safety checks always so they survive any --tags selection that later tasks depend on.
  • Hide destructive or rare tasks behind never so they run only when explicitly requested by name.
  • Confirm a selection with --list-tasks before running --tags / --skip-tags against anything that matters, so you see exactly what executes.
  • Keep a small, consistent tag vocabulary — config, deploy, packages — across roles, so the same tag means the same phase everywhere.
Comparable tools Terraform -target selects resources by address — a graph operation, not a label Make targets select by name, not by tag No direct equivalent — tags are an Ansible-internal selection mechanism

Knowledge Check

You run a playbook with --tags config. What happens to a task that has no tag at all?

  • It is skipped — under --tags, only tasks carrying a matching tag run, so an untagged prerequisite is silently omitted
  • It runs anyway, because every untagged task is treated as if it were implicitly tagged with each and every tag that exists
  • The play aborts with an error, flatly refusing to run until every single task in it has been given a tag
  • It runs first, because untagged tasks are automatically prioritized and moved ahead of any tagged ones under --tags

What is the difference between the always and never tags?

  • always runs on every invocation regardless of --tags; never runs only when explicitly named with --tags
  • always forces a task to run first and never forces it to run last, controlling execution order rather than selection
  • never is the tag that runs on every single invocation, while always is the one that must be explicitly requested by name
  • Both are interchangeable aliases that simply hide a task from the output of --list-tags

A teammate expects --tags to make a tagged dependency run before an untagged one it relies on. Why is that wrong?

  • Tags select which tasks run, not the order — and the untagged dependency is skipped entirely, not reordered
  • Tags actively reverse the entire play order, so the tagged dependency would end up running dead last instead of first
  • Tags only ever affect whole roles and never individual tasks, so the ordering of the dependency is left completely unaffected
  • The dependency quietly runs twice under --tags, which masks the underlying ordering problem from view

What does --list-tasks give you before a tagged run against production?

  • A preview of exactly which tasks the current --tags selection would execute, so you can confirm it hits what you intend
  • A dry run that actually applies all the selected tasks on the host and then cleanly rolls every one of them back afterward
  • A complete list of every host the play would touch, neatly grouped under each tag it matches
  • A line-by-line diff of the exact changes each tagged task would make on disk before they happen

You got correct