Ansible Tags

Run a targeted subset of a large playbook without touching everything else.

What tags are and why you use them

A tag is a label you attach to a task, play, or role call. When you run ansible-playbook with --tags, Ansible only executes tasks that carry at least one of the specified tags. Everything else is skipped.

This is essential in production because large playbooks that configure many services can take 10–20 minutes to run completely. When you only changed one variable in the chrony config, you don't want to wait for nginx, postfix, and everything else to also run. Tags let you run just the affected service's tasks.

# Without tags — runs everything (slow, touches many services)
ansible-playbook -i inventories/production/ site.yml

# With tags — only runs tasks tagged 'chrony' (fast, precise)
ansible-playbook -i inventories/production/ site.yml --tags chrony

Applying tags to tasks, plays, and roles

Tags on individual tasks

# Single tag
- name: Install chrony
  ansible.builtin.package:
    name: chrony
    state: present
  tags: chrony

# Multiple tags — task runs if any of them match
- name: Deploy chrony config
  ansible.builtin.template:
    src: chrony.conf.j2
    dest: /etc/chrony.conf
  tags:
    - chrony
    - config

Tags on a play

# All tasks in this play inherit the 'webservers' tag
- name: Configure web servers
  hosts: webservers
  tags: webservers
  roles:
    - nginx
    - certbot

Tags on a role call in the roles: section

- name: Configure all services
  hosts: all
  roles:
    - role: chrony
      tags: chrony         # all tasks inside this role get the 'chrony' tag

    - role: nginx
      tags: nginx

    - role: postfix
      tags: postfix

This is the standard pattern in an infra repo's site.yml: each role call gets a tag matching the role name. It lets you run a single role across the whole inventory with one command.

Tags on import_role and include_role

# Tagging an import_role block
- name: Apply chrony role
  ansible.builtin.import_role:
    name: chrony
  tags: chrony

# Tagging an include_role block
- name: Apply chrony role conditionally
  ansible.builtin.include_role:
    name: chrony
  tags: chrony
  when: configure_ntp | bool

The behaviour of tags on these two differs significantly — see import_role vs include_role below.

--tags and --skip-tags on the command line

# Run only tasks tagged 'config'
ansible-playbook site.yml --tags config

# Run tasks tagged 'install' OR 'config' (comma-separated = OR)
ansible-playbook site.yml --tags install,config

# Skip tasks tagged 'verify' and run everything else
ansible-playbook site.yml --skip-tags verify

# Combine: run chrony tasks, but skip the verify step
ansible-playbook site.yml --tags chrony --skip-tags verify

# Limit to specific hosts AND specific tags
ansible-playbook site.yml --limit web01 --tags nginx,config
--tags is OR, not AND. --tags install,config runs tasks tagged install or config or both. There is no built-in way to run only tasks that have both tags simultaneously.

--list-tags — discover what a playbook exposes

Before running with --tags, use --list-tags to see every tag that has been applied across the playbook and all its roles.

# List all tags defined in site.yml and all included roles/tasks
ansible-playbook site.yml --list-tags

# Output example:
# playbook: site.yml
#
#   play #1 (all): Configure all hosts
#     TASK TAGS: [chrony, config, install, nginx, postfix, service, verify]

# See tags alongside task names (combine with --list-tasks)
ansible-playbook site.yml --list-tasks --tags chrony

Run --list-tags when joining a new repo to understand what tag vocabulary the team uses before your first --tags run.

always and never special tags

Two tag values have special meaning in Ansible:

always — run even when other --tags are specified

# This task always runs, even when --tags chrony is used
- name: Pre-flight checks
  ansible.builtin.assert:
    that:
      - ansible_os_family in ['RedHat', 'Debian']
      - env is defined
    fail_msg: "Required variables missing or unsupported OS"
  tags: always       # guarantees pre-flight runs on every tagged run

Use always for:

never — only run when explicitly requested

# This task is skipped by default and during normal tagged runs
# Only runs when you specifically ask for it
- name: Wipe application data
  ansible.builtin.file:
    path: /var/lib/myapp/data
    state: absent
  tags: never, wipe-data

# To invoke it:
# ansible-playbook site.yml --tags wipe-data

Use never for:

Multiple tags including never: tags: never, wipe-data means the task is skipped by default AND requires the explicit wipe-data tag to run. The never tag overrides always and any other tag.

Tags with import_role vs include_role

This is one of the most important and least obvious differences between import_role and include_role. Getting it wrong means your --tags silently does nothing.

import_role — tags propagate into the role (static)

- name: Apply chrony role
  ansible.builtin.import_role:
    name: chrony
  tags: chrony

Because import_role is resolved at parse time, Ansible can see all the tasks inside the role before execution starts. The chrony tag is applied to every task inside the role. Running --tags chrony runs all of them.

include_role — tags do NOT propagate into the role (dynamic)

- name: Apply chrony role conditionally
  ansible.builtin.include_role:
    name: chrony
  tags: chrony

Because include_role is resolved at runtime, Ansible cannot see inside the role during parsing. The tag is applied to the include_role task itself, not to the tasks inside the role. Running --tags chrony will include the role but then run all tasks inside it (ignoring any inner tags).

Practical rule: If you need --tags to work correctly with role contents, use import_role. Only use include_role when you need dynamic behaviour (loops, conditionals at role level) and accept that inner tags won't be filterable from outside.
import_roleinclude_role
Tags on the call propagate inside?YesNo
--tags filters inner tasks?YesNo (all inner tasks run)
Supports loop: on the call?NoYes
Supports when: evaluated per-host?PartialYes

Infra tag design pattern

A consistent tagging vocabulary across all roles makes a large playbook much easier to operate. The following convention is common in infrastructure repos and maps to the natural phases of role execution.

Standard phase tags

TagApplied toUse case
installPackage installation tasksRe-run package install without touching config
configTemplate and copy tasksRe-deploy config files after a variable change
serviceEnable/start/restart service tasksFix a service state without re-rendering config
verifyPost-deploy health check tasksRun checks only, skip deployment tasks
alwaysPre-flight assert tasksRuns on every tagged invocation

Applying the pattern inside a role

---
# roles/chrony/tasks/main.yml

- name: Include pre-flight checks
  ansible.builtin.import_tasks: preflight.yml
  tags: always                    # assertions run on every tagged run

- name: Include install tasks
  ansible.builtin.import_tasks: install.yml
  tags:
    - chrony
    - install

- name: Include config tasks
  ansible.builtin.import_tasks: config.yml
  tags:
    - chrony
    - config

- name: Include service tasks
  ansible.builtin.import_tasks: service.yml
  tags:
    - chrony
    - service

- name: Include verify tasks
  ansible.builtin.import_tasks: verify.yml
  tags:
    - chrony
    - verify

Each task file gets both the role name tag (chrony) and the phase tag (config). This lets you target a specific role (--tags chrony) or a specific phase across all roles (--tags config).

Role call in site.yml

---
- name: Configure all services
  hosts: all
  become: true

  roles:
    - role: chrony
      tags: chrony

    - role: nginx
      tags: nginx

    - role: postfix
      tags: postfix

Practical example: config-only run

You updated group_vars/all/vars.yml to change the chrony NTP server. You want to re-deploy only the chrony config file across production, without touching installs, service restarts on other roles, or verify tasks.

# See what tasks would run first (dry-run + task list)
ansible-playbook -i inventories/production/ site.yml \
  --tags chrony,config \
  --list-tasks

# Output:
# TASK [chrony : Pre-flight checks] (always)
# TASK [chrony : Deploy chrony.conf]
# TASK [chrony : Deploy chrony keys fragment]

# Confirm the diff before applying
ansible-playbook -i inventories/production/ site.yml \
  --tags chrony,config \
  --check --diff

# Apply
ansible-playbook -i inventories/production/ site.yml \
  --tags chrony,config

The workflow is always the same: list → diff → apply. Tags make each step fast and safe by limiting scope to exactly what changed.

# Common single-role runs
ansible-playbook site.yml --tags nginx          # full nginx role (all phases)
ansible-playbook site.yml --tags nginx,config   # nginx config only
ansible-playbook site.yml --tags nginx,verify   # nginx verify only (health checks)

# Phase across all roles (e.g. after a mass variable update)
ansible-playbook site.yml --tags config         # re-deploy all config files

# Skip slow verify tasks on a routine run
ansible-playbook site.yml --skip-tags verify
Related: See Ansible Roles in Practice for the import_role vs include_role comparison, and Ansible Debugging for --list-tasks and --list-tags usage.