Skip to main content
RunBook Academy

AnsibleXXIII · Tags, Blocks and Error HandlingError handling

Tags as an operational control

Intermediate⏱ ~19 minansible-playbook

What you'll learn

  • Apply tags at each level and predict how they combine
  • State the precedence rule between --tags and --skip-tags
  • Verify a tag selection before relying on it in production
  • Recognise a tag selection that matched nothing and reported success

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Tags get introduced as an organisational feature — a way to label tasks so you can find them. That framing produces the tag schemes that cause incidents.

Tags are a blast-radius control. --tags certificates is a statement that this run will touch certificate configuration and nothing else, on hosts that may be serving traffic. Every property that matters follows from treating them that way: the selection has to be verifiable before the run, it has to be the same tomorrow as today, and a selection that matches nothing must not look like a selection that worked.

That last one is where Ansible will not help you, and it is the reason this lesson exists.

Where tags can go

ansible-doc -t keyword tags states the scope exactly:

Read-only / Safethe tags keyword
$ ansible-doc -t keyword tags
tags:
applies_to:
- Play
- Role
- Block
- Task
- Handler
description: Tags applied to the task or included tasks, this allows selecting subsets
  of tasks from the command line.
priority: 0
template: explicit
type: list

Tags combine downward and additively. A tag on a play applies to every task in it; a task’s own tags are added rather than replacing:

Read-only / Safea play-level tag plus a task-level tag
$ ansible-playbook -i inventory.ini playtags.yml --list-tasks
playbook: playtags.yml

play #1 (local): tagged play	TAGS: [playwide]
  tasks:
    task in tagged play	TAGS: [playwide]
    task with its own tag	TAGS: [inner, playwide]

The same additive inheritance runs through import_role, import_tasks, the roles: keyword and meta/main.yml dependencies — Part XXII lessons 5 and 6 show each of those executed. The one place it does not flow is through a dynamic include, which is Part XXIII lesson 3 and is a large enough problem to get its own lesson.

Selecting

Two options, and a rule people get backwards.

--tags certificates            run only tasks carrying this tag
--tags certificates,nginx      run tasks carrying either
--skip-tags migrations         run everything except tasks carrying this

--skip-tags always wins. A task carrying both a selected tag and a skipped one is skipped:

Read-only / Safeskip beats select
$ ansible-playbook -i inventory.ini import-local.yml --tags patch --skip-tags alpha_inner
TASK [alpha : alpha task two] **************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

This is the safe direction for the rule to run. --skip-tags is how an operator excludes something they know is dangerous right now — the database migration, the reboot — and an exclusion that could be overridden by a selection would be useless as a safety control.

The failure: a tag that selected nothing

Ansible does not warn when --tags matches no tasks. The run completes, the recap is clean, and the exit code is zero:

Read-only / Safe--tags with a tag that does not exist anywhere
$ ansible-playbook -i inventory.ini specialtags.yml --tags no_such_tag
TASK [always task] *************************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

No warning. No error. Exit code 0.

Now put that in a pipeline. A CI job runs ansible-playbook site.yml --tags deploy, somebody renames the tag to deployment in a refactor, and the job keeps passing. Every build reports a successful deployment. Nothing has been deployed since the rename.

The defence is one command, and it is the reason --list-tags exists:

Read-only / Safewhat tags does this playbook actually have
ansible-playbook -i inventory/production.ini site.yml --list-tags
Read-only / Safewhat would this selection actually run
ansible-playbook -i inventory/production.ini site.yml --list-tasks --tags certificates

The second one is the pre-flight check to build a habit around. An empty task list is the answer to “did my tag select anything”, delivered before the run rather than inferred from a suspiciously quiet recap afterwards.

Designing a tag scheme that survives

Three properties, in order of how often they are missing.

Tags name work, not files. certificates, packages, firewall, migrations. Not step2, not webapp_role, not phase_a. The name has to mean something to the operator who types it at 03:00 without reading the playbook.

A tag selects a subset of the full run, not a different run. This is the constraint that keeps tags testable. If --tags foo executes tasks that the untagged run does not — because a when reads the tag selection, or because a task is tagged never and pulled in — then --tags foo is a separate code path with its own untested behaviour. Nobody can say what it does without running it, and running it is the thing they were trying to be careful about.

Every tag appears in --list-tags. Which means: no tags that exist only inside dynamic includes. Lesson 3.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A CI job runs ansible-playbook site.yml --tags deploy. A refactor renames the tag to deployment. What does the job do on its next run?

  2. Q2. Which levels accept a tags keyword, such that tasks beneath them inherit it? Select all that apply.

  3. Q3. A task carrying both a tag named in --tags and a tag named in --skip-tags is executed, because an explicit selection overrides an exclusion.

  4. Q4. Why is a tag whose selection runs tasks the untagged run would not run considered a design failure?

Passing score: 75%. Answers are checked in this browser.