Tags: Running Part of a Playbook
Scenario: During a change freeze, a single log-level setting needs pushing. The only available option is to re-run the whole playbook, which would also reinstall packages and redeploy the application.
New words, in plain English
- Tag - a label attached to a task, block, role or play.
--tags- run only tasks carrying at least one of these tags.--skip-tags- run everything EXCEPT tasks carrying these tags.always- a special tag: the task runs unless explicitly skipped.never- a special tag: the task runs only when its tag is explicitly requested.
Tags let one playbook be run in slices. This matters operationally far more than it sounds: a change freeze, a package mirror outage, or a five-minute config fix at 2 AM all become safe when you can run exactly the part you need.
Tags apply to tasks, blocks, roles and plays, and they inherit downward - tagging a block tags every task inside it, which is much more maintainable than tagging tasks individually.
Two special tags carry real meaning. always makes a task run under any tag filter unless explicitly skipped - correct for audit logging, for loading variables that everything else depends on, and for pre-flight assertions. never makes a task run only when someone asks for its tag by name - correct for a destructive or very slow operation you want available but never accidental.
The common trap is a task filtered out by a tag while a later task still depends on its registered result. If a --tags config run skips the task that gathers a value, the task that uses it fails. Tag related work as a unit, and tag variable-loading tasks always.
Analogy: Tags are chapter markers on a recording. You can jump to the chapter you need instead of watching the whole thing - but if chapter four refers to a diagram introduced in chapter two, jumping straight to four will not make sense.
A worked example
- name: Web tier maintenance
hosts: webservers
pre_tasks:
- name: Record that a maintenance run happened
ansible.builtin.lineinfile:
path: /var/log/ansible-runs.log
create: true
line: "{{ ansible_date_time.iso8601 | default('run') }}"
tags: [always] # runs under EVERY tag filter
tasks:
- name: Install the package set
ansible.builtin.package:
name: "{{ app_packages }}"
tags: [packages]
- name: Push the application configuration
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
tags: [config]
- name: Deploy the release
ansible.builtin.unarchive:
src: "app-{{ app_version }}.tar.gz"
dest: /srv/app
tags: [deploy]
- name: Wipe and rebuild the cache (slow, destructive)
ansible.builtin.command: /opt/bin/rebuild-cache.sh
tags: [never, cache-rebuild] # ONLY when asked for by name
# ansible-playbook site.yml --tags config
# ansible-playbook site.yml --skip-tags packages
# ansible-playbook site.yml --tags cache-rebuild
--list-tags prints every tag a playbook defines, and --list-tasks --tags config shows exactly which tasks a tag filter would run. Both are free, and both settle the question of what a partial run will actually do far better than reading the file.
A design note: tags are an operational interface, so they deserve the same stability as any other interface. Renaming a tag breaks the runbook that someone wrote against it. Keep the set small and meaningful - packages, config, deploy - rather than tagging every task with its own name, which produces a hundred tags that nobody can use.
Warning: Under --tags, a task whose registered result a later task needs may be filtered out - and the later task then fails on an undefined result. Tag related work as a unit.
Goal: Put this to work in the ansible-tags-partial-run lab. Open/labs/ansible, pickansible-tags-partial-run, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.