07 · Tags, Limits and Targeting¶
A playbook that configures a whole fleet is the wrong thing to run when you want to change one setting on one host. Ansible gives you three ways to narrow a run: tags (which tasks), limits (which hosts) and check mode (whether to change anything). Used well they make a large playbook safe to run; used carelessly they cause partial, inconsistent states.
What was run
The tag and limit results below were produced by a real run of src/l2/tags.yml against two local hosts, and the exit-status checks come from the shell. Behaviour is for ansible-core 2.21.5.
Tags: which tasks run¶
tasks:
- ansible.builtin.debug: {msg: "install"}
tags: [install, setup]
- ansible.builtin.debug: {msg: "configure"}
tags: [configure, setup]
- ansible.builtin.debug: {msg: "always runs"}
tags: [always]
- ansible.builtin.debug: {msg: "never unless asked"}
tags: [never, debug_only]
- ansible.builtin.debug: {msg: "untagged"}
Measured results (messages printed on host w1):
| Command | Tasks that ran |
|---|---|
| (no options) | install, configure, always runs, untagged |
--tags configure |
configure, always runs |
--skip-tags setup |
always runs, untagged |
--tags debug_only |
always runs, never unless asked |
--tags nonexistent |
always runs |
--tags setup --skip-tags configure |
install, always runs |
Rules shown by the table:
- No options runs untagged and tagged tasks alike, except tasks tagged
never. --tags Xruns only tasks with tag X, so untagged tasks do not run; this is a frequent surprise when you tag only some tasks.alwaystasks run regardless of--tags(not if you--skip-tags always). Use it for tasks other tasks depend on, such as loading variables or fact gathering helpers.nevertasks are excluded unless you explicitly select another tag they carry. Good for destructive or diagnostic tasks.--skip-tagswins over--tagswhen both match the same task.- A task selected by tag still has its
when:evaluated. --list-tagsshows the tag set:[always, configure, debug_only, install, never, setup].
Tag hygiene¶
Tags are easy to misuse. A tag selects a task but does not include its dependencies: --tags configure skips an earlier install task. If configure needs a variable set by an untagged task earlier, it will fail in an odd way. Keep tagged tasks independent, or tag the prerequisite always. For roles, tags set on the role entry apply to all of the role's tasks; import_role tags are inherited statically; include_role needs apply: {tags: ...}. Use a small, documented vocabulary (packages, config, service, users) rather than a tag per task.
Do not make tags a substitute for structure. A playbook that has to be run with the right tags to work is a playbook that should be split into several playbooks or roles.
Limits: which hosts run¶
--limit narrows the hosts of every play to an intersection with the given pattern:
--limit w1or-l w1: one host.--limit 'web:!w2': pattern expression (Lesson 03).--limit @hosts.txt: hosts listed one per line in a file. Running with--limit @lim.txt(containingw2) executed onw2only:ok: [w2].--limit nope(matches nothing): printedSpecified inventory, host pattern and/or --limit leaves us with no hosts to target.and exited with status 1. This is different from a play whosehosts:matches nothing, which exits 0 (Level 1 · 03). A--limitthat leaves nothing is treated as an error, which is the safer behaviour.
--limit with serial and run_once interacts: run_once runs on the first host of the limited set, so limiting can change which host performs the one-time action. Test that if you rely on it.
A very common operational workflow:
ansible-playbook site.yml --limit canary --check --diffto preview.ansible-playbook site.yml --limit canaryto apply to a canary host or group.- Verify (smoke tests, monitoring).
ansible-playbook site.yml --limit 'prod:!canary'for the rest, perhaps in batches withserial(Lesson 08).
--check, --diff, --step, --start-at-task¶
Check mode (Level 1 · 05) combines with limits and tags for targeted previews: --tags config --limit web --check --diff answers "what would a config change do to the web hosts?" without touching anything. Be aware of the caveat from Level 1: tasks that depend on earlier tasks' real effects (a package install) can mislead a check run that skips them.
--step prompts before each task. --start-at-task resumes at a named task but skips the earlier ones, which broke the sandbox setup in Level 1 · 09.
Variables that depend on the run¶
Magic variables expose the selection: ansible_run_tags, ansible_skip_tags, ansible_limit, ansible_check_mode, ansible_play_hosts, ansible_play_batch. You can make playbooks refuse to run unsafely:
- ansible.builtin.assert:
that: ansible_limit is defined
fail_msg: "Refusing to run against production without --limit. Use --limit canary first."
run_once: true
when: "'production' in group_names"
Guard rails like this are cheap; the cost is a little friction. I checked the variables: with no options ansible_limit was undefined (so is defined works as a test), ansible_run_tags was ['all'], ansible_skip_tags was [] and ansible_check_mode was False; with --limit n2 --tags all --skip-tags x --check they were n2, ['all'], ['x'] and True. The assert guard itself was not run.
Separate playbooks vs tags¶
A playbook per operation (site.yml, deploy.yml, rotate-keys.yml, patch.yml) is easier to understand and review than one giant playbook with twenty tags. Use tags for modest sub-selection inside a playbook. Build an index (a README or Makefile targets) of the commands people should run, so operators do not invent their own tag combinations.
How It Actually Works¶
Tag selection is applied when the play's task list is compiled for execution: each task has a set of tags (its own plus those inherited from blocks, roles and play), and the engine keeps a task if the run's tag filter says so. always and never are special tags handled in that filter. Limits are applied earlier, when the play's host pattern is evaluated: the effective host list is the intersection of the pattern and the limit. If the intersection is empty and a limit was given, the CLI aborts with the error shown above; if the pattern alone matched nothing, the play is skipped. The restriction is applied on the controller, so tasks excluded by tags or limits never reach a target and no connection is made to hosts outside the limit (which is one reason limits are also a performance tool).
Common mistakes¶
- Expecting
--tagsto run untagged setup tasks. - Tagged tasks with hidden dependencies on untagged ones.
--limitforgotten, so production is changed along with the intended host.- Different behaviour assumed between an empty
hosts:match and an empty--limit. - Tags that mean different things in different roles.
- Using tags where separate playbooks would be clearer.
- No guard rails against running on production without limits.
Exercise¶
- Reproduce the table with your own playbook and add a
--tagscombination you have not seen; predict the result first. - Show that
--limitwith a typo exits non-zero and that ahosts:typo does not; add a CI pre-step that catches both. - Write a guard-rail assertion that fails when the play would touch more than five hosts without an explicit
-e confirm=yes. - Convert a tag-heavy playbook into three smaller playbooks and a
Makefilewith the commands operators should run.