Introduction
Tags let you run a subset of tasks from a playbook. Instead of running everything, target only what you need — just the config deployment, just the packages, or just the service restarts. Essential for large playbooks and iterative development.
Tag a Task
tasks:
- name: Install packages
ansible.builtin.apt:
name: [nginx, redis]
tags: packages
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: config
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
tags: services
Run and Skip Tags
# Run only tasks tagged 'config'
ansible-playbook site.yml --tags config
# Run multiple tags
ansible-playbook site.yml --tags "config,services"
# Skip specific tags
ansible-playbook site.yml --skip-tags packages
# List all tags in a playbook
ansible-playbook site.yml --list-tags
# List tasks that would run
ansible-playbook site.yml --tags config --list-tasks
Multiple Tags on a Task
- name: Deploy app config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/config.yml
tags:
- config
- deploy
- myapp
# This task runs with --tags config OR --tags deploy OR --tags myapp
Special Tags
always
Tasks tagged always run even when you use --tags (unless explicitly skipped):
- name: Gather facts
ansible.builtin.setup:
tags: always # Runs no matter what
- name: Install packages
ansible.builtin.apt:
name: nginx
tags: packages
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: config
ansible-playbook site.yml --tags config
# Runs: "Gather facts" (always) + "Deploy config" (config)
ansible-playbook site.yml --skip-tags always
# Skips "Gather facts"
never
Tasks tagged never only run when explicitly requested:
- name: Dangerous cleanup
ansible.builtin.shell: rm -rf /tmp/old-data
tags: never # Only runs with --tags cleanup
- name: Actually give it a usable tag too
ansible.builtin.shell: rm -rf /tmp/old-data
tags:
- never
- cleanup
ansible-playbook site.yml # Does NOT run cleanup
ansible-playbook site.yml --tags cleanup # DOES run cleanup
Tag a Block
- name: Database tasks
tags: database
block:
- name: Install PostgreSQL
ansible.builtin.apt:
name: postgresql
- name: Configure PostgreSQL
ansible.builtin.template:
src: pg_hba.conf.j2
dest: /etc/postgresql/16/main/pg_hba.conf
- name: Start PostgreSQL
ansible.builtin.service:
name: postgresql
state: started
# All three tasks inherit the 'database' tag
Tag a Role
roles:
- role: nginx
tags: [webserver, nginx]
- role: postgresql
tags: [database, postgresql]
- role: monitoring
tags: monitoring
# Run only the nginx role
ansible-playbook site.yml --tags nginx
# Run webserver and database roles
ansible-playbook site.yml --tags "webserver,database"
Tag a Play
---
- name: Web server setup
hosts: webservers
tags: web
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
- name: Database setup
hosts: dbservers
tags: db
tasks:
- name: Install PostgreSQL
ansible.builtin.apt:
name: postgresql
ansible-playbook site.yml --tags web # Only web play runs
Tag with include/import
# import_tasks — tags apply to ALL imported tasks
- name: Import web tasks
ansible.builtin.import_tasks: web.yml
tags: web
# Every task in web.yml gets the 'web' tag
# include_tasks — tag applies to the include itself
- name: Include web tasks
ansible.builtin.include_tasks: web.yml
tags: web
# Only the include task is tagged; tasks inside web.yml keep their own tags
This is an important distinction:
| import_tasks | include_tasks | |
|---|---|---|
| Tag inheritance | Tags flow to all imported tasks | Tags only on the include line |
| When evaluated | Parse time (static) | Runtime (dynamic) |
| Best for | Applying tags to a whole file | Conditional includes |
Practical Patterns
Deployment Workflow
tasks:
- name: Pull latest code
ansible.builtin.git:
repo: https://github.com/myorg/myapp.git
dest: /opt/myapp
tags: [deploy, code]
- name: Install dependencies
ansible.builtin.pip:
requirements: /opt/myapp/requirements.txt
tags: [deploy, deps]
- name: Run migrations
ansible.builtin.command: python manage.py migrate
args:
chdir: /opt/myapp
tags: [deploy, migrate]
- name: Deploy config
ansible.builtin.template:
src: config.yml.j2
dest: /opt/myapp/config.yml
tags: [deploy, config]
- name: Restart app
ansible.builtin.service:
name: myapp
state: restarted
tags: [deploy, restart]
# Full deploy
ansible-playbook site.yml --tags deploy
# Just update config and restart
ansible-playbook site.yml --tags "config,restart"
# Just run migrations
ansible-playbook site.yml --tags migrate
Debug Tag
- name: Show all variables
ansible.builtin.debug:
var: hostvars[inventory_hostname]
tags:
- never
- debug
- name: Show network info
ansible.builtin.debug:
var: ansible_default_ipv4
tags:
- never
- debug
# Normal run — debug tasks don't run
ansible-playbook site.yml
# Debug run — includes debug output
ansible-playbook site.yml --tags "all,debug"
Best Practices
- Use consistent tag names across playbooks:
packages,config,services,deploy,security - Tag at the right level — don't over-tag individual tasks; tag blocks or roles
- Use
alwayssparingly — only for truly essential tasks (fact gathering, validation) - Use
neverfor dangerous tasks — cleanup, destructive operations - Document your tags — list them in README
Troubleshooting
Tagged Task Doesn't Run
Check if the task is inside an include_tasks — tags don't flow into dynamic includes:
# Tags DON'T flow in
- include_tasks: tasks.yml
tags: mytag
# Tasks inside tasks.yml won't have 'mytag'
# Tags DO flow in
- import_tasks: tasks.yml
tags: mytag
# All tasks inside tasks.yml get 'mytag'
Handler Not Triggered with --tags
Handlers only run if their notifying task ran AND changed something. If you skip the notifying task with --tags, the handler won't trigger.
Related Articles
Conclusion
Tags give you surgical control over playbook execution. Tag tasks, blocks, roles, and plays — then use --tags and --skip-tags to run exactly what you need. Use always for essential tasks that must run regardless, and never for dangerous operations that require explicit opt-in. Remember that import_tasks inherits tags (static) while include_tasks doesn't (dynamic). Keep tag names consistent across your project for predictable behavior.