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

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, pick ansible-tags-partial-run, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.