AnsibleXXIII · Tags, Blocks and Error HandlingError handling
Tags as an operational control
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
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:
$ ansible-doc -t keyword tagstags:
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: listTags 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:
$ ansible-playbook -i inventory.ini playtags.yml --list-tasksplaybook: 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:
$ ansible-playbook -i inventory.ini import-local.yml --tags patch --skip-tags alpha_innerTASK [alpha : alpha task two] **************************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0This 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:
$ ansible-playbook -i inventory.ini specialtags.yml --tags no_such_tagTASK [always task] *************************************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0No 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:
ansible-playbook -i inventory/production.ini site.yml --list-tagsansible-playbook -i inventory/production.ini site.yml --list-tasks --tags certificatesThe 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
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?
Q2. Which levels accept a tags keyword, such that tasks beneath them inherit it? Select all that apply.
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.
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.