Ansible Tags Survival Guide: How We Cut 20-Minute Playbooks to 90 Seconds
Master Ansible tags for selective execution: --tags, --skip-tags, always/never, tag inheritance, CI integration.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
Use --tags to run only tagged tasks: ansible-playbook playbook.yml --tags deploy
Use --skip-tags to exclude tasks: ansible-playbook playbook.yml --skip-tags smoke-test
Tag tasks, roles, and blocks with tags: key; blocks inherit tags to all children
Special tags always and never control unconditional execution: tags: always runs even with --tags foo
Tags on includes/imports propagate to all contained tasks; use apply: to override
In CI, use --tags changed with a custom callback to run only modified components
Avoid tag typos: Ansible silently ignores unknown tags, use --list-tags to verify
Use --list-tasks --tags deploy to preview which tasks will execute
Imagine you're a chef with a massive recipe book. Normally, you cook the entire meal from appetizer to dessert, which takes hours. But sometimes you only need to reheat the soup because the rest is already perfect. Ansible tags are like sticky notes on each recipe step: you can say 'only do the steps tagged with #reheat.' This saves you from starting the whole cooking process over. In our kitchen, we used this to skip running full server setups when only updating a config file—cutting our deployment time from 20 minutes to 90 seconds.
I still remember the Friday afternoon when our deployment pipeline broke. The playbook took 20 minutes to run, and the team was waiting to push a critical hotfix. The problem? We had no way to skip the 15-minute OS patch check that ran every single time, even when we only changed a config file. That's when I discovered Ansible tags—and it changed everything. Tags let you selectively execute parts of your playbook, turning a monolithic run into a surgical strike. In this article, I'll share how we use tags in production, including a real incident where a missing tag caused a 2-hour outage, and how to integrate tags with CI to run only changed components.
Tagging Tasks, Roles, and Blocks
Tagging is straightforward: add a tags: key to a task, role, or block. For tasks:
``yaml - name: Install nginx apt: name: nginx state: present tags: - packages - nginx ``
For roles, you can tag the role itself in the playbook:
``yaml - hosts: webservers roles: - role: nginx tags: - nginx - web ``
Blocks inherit tags to all tasks inside:
``yaml - name: Configure web server block: - name: Copy config template: ... - name: Restart service service: ... tags: - config ``
A gotcha: if you tag a block, the block itself is not a task, so --list-tasks won't show the block tag unless a task inside has it. Always tag individual tasks if you need precise control.
--tags config, a handler without the config tag won't run even if notified.deploy, config, security, or monitoring. This lets us run --tags deploy during a code push, skipping security scans that take 5 minutes.Using --tags and --skip-tags CLI Flags
The --tags flag filters execution to only tasks with specified tags. Multiple tags are comma-separated: --tags deploy,config. The --skip-tags flag excludes tasks with those tags. You can combine them: --tags deploy --skip-tags smoke-test runs deploy tasks but not smoke tests.
Important: --tags and --skip-tags are mutually exclusive in the sense that --skip-tags takes precedence. If a task has both a tag you include and a tag you skip, it will be skipped.
Use --list-tasks --tags deploy to preview what will run. This is invaluable for debugging: I once spent an hour wondering why a task wasn't running, only to find a typo in the tag name (deploy vs deply).
In CI, you can pass tags dynamically:
``bash ansible-playbook playbook.yml --tags "$TAGS" ``
Where $TAGS is set by your CI system based on changed files.
ansible-playbook playbook.yml --list-tags to see all tags in your playbook. This helps avoid typos and confirms your tagging strategy.--tags deploy but a new task was tagged as deploy (misspelled). The task never ran, causing a config drift. Now we run --list-tags in CI to validate tags.--list-tasks --tags before running; tag names are case-sensitive and typos are silent.Special Tags: always and never
Ansible provides two special tags: always and never. Tags with always run regardless of the --tags filter (unless --skip-tags always is used). Tags with never are skipped unless explicitly included with --tags never.
Use always for critical setup tasks like creating users or directories that must exist for other tasks to work. Use never for optional or destructive tasks, like database resets.
Example:
```yaml - name: Create app user user: name: app tags: always
- name: Reset database
- shell: dropdb myapp
- tags: never
- ```
Gotcha: always does not override --skip-tags always. If you skip always, it won't run. Also, always tasks are not included when you use --tags without including always explicitly? Actually, they are included automatically. But if you use --tags foo, tasks with always still run. This is by design: always is additive.
However, if you use --skip-tags always, they are skipped. So be careful when skipping.
always tasks run even with --tags foo; never tasks only run with --tags never or --tags all.always because it's critical for disk space. One time a junior engineer used --skip-tags always to speed up a test and the disks filled up, causing an outage.always for essential tasks, never for optional ones; remember --skip-tags always will skip them.Tag Inheritance in Includes and Imports
When you include or import a file, tags can be applied at the include/import level. These tags propagate to all tasks in the included file. However, there's a nuance: include_tasks vs import_tasks.
With import_tasks, tags are applied statically at parse time. With include_tasks, tags are dynamic but still propagate.
Example:
``yaml - name: Include deploy tasks import_tasks: deploy.yml tags: - deploy ``
All tasks in deploy.yml will inherit the deploy tag. You can override this by using the apply keyword:
``yaml - name: Include with custom tags import_tasks: deploy.yml tags: - deploy apply: tags: - custom ``
This applies custom to all tasks, but they still also have deploy? Actually, apply replaces the inherited tags. So tasks will have only custom tag. This is a common source of confusion.
Best practice: Use import_tasks for static includes and apply tags at the include level. For dynamic includes, test carefully because tag inheritance can be unpredictable.
apply key on includes replaces the inherited tags, it does not add to them. If you want both, list all tags in apply.tags: always. Someone added apply: tags: deploy thinking it would add, but it replaced always, so critical setup tasks were skipped during --tags deploy runs.apply carefully as it overrides inherited tags.Strategy for Tagging in Large Playbooks
In large playbooks with hundreds of tasks, a consistent tagging strategy is essential. Here's our production approach:
- Categorize by function:
deploy,config,security,monitoring,cleanup. - Categorize by component:
nginx,app,db,cache. - Use compound tags:
deploy_app,config_nginx,security_patch. - Tag every task with at least one function tag.
- Use
alwayssparingly—only for tasks that must run every time (e.g., user creation). - Document tags in a README or use
--list-tagsin CI to generate docs.
Example tag structure:
``yaml tags: - deploy - app - config ``
This allows running --tags deploy for a full deploy, or --tags app for app-specific tasks, or --tags deploy,app for app deploy only.
Production insight: We maintain a tags.yml file that lists all tags and their purpose, checked into the repo. New team members must read it before modifying playbooks.
deploy_app is better than dplapp.--tags deploy.Using Tags in CI to Run Only Changed Components
In CI, you can determine which components changed and pass corresponding tags to Ansible. For example, if only the nginx directory changed, run only tasks tagged with nginx.
Here's a GitHub Actions workflow snippet:
```yaml - name: Detect changes id: changed uses: dorny/paths-filter@v2 with: filters: | nginx: - 'roles/nginx/' app: - 'app/' db: - 'roles/db/**'
- name: Run Ansible with tags
- run: |
- TAGS=""
- if [[ "${{ steps.changed.outputs.nginx }}" == "true" ]]; then TAGS="$TAGS,nginx"; fi
- if [[ "${{ steps.changed.outputs.app }}" == "true" ]]; then TAGS="$TAGS,app"; fi
- if [[ "${{ steps.changed.outputs.db }}" == "true" ]]; then TAGS="$TAGS,db"; fi
- TAGS=${TAGS#,}
- ansible-playbook playbook.yml --tags "$TAGS"
- ```
If no tags are set (no changes), you might want to run a default set of tags, or skip entirely. Use --tags always to run only always tasks, or --tags never to run nothing.
Gotcha: If you use --tags with an empty string, Ansible runs all tasks. So ensure you handle the empty case.
Tagging Handlers Correctly
Handlers are tasks that run when notified, but they have their own tags. If a handler is not tagged, it will only run if no --tags filter is applied. If you use --tags deploy, an untagged handler will not run even if notified.
Solution: Tag handlers with the same tags as the tasks that notify them, or use tags: always for critical handlers.
Example:
``yaml handlers: - name: restart nginx service: name: nginx state: restarted tags: - nginx - config ``
Now if you run --tags nginx, this handler will run when notified.
Gotcha: Even with correct tags, handlers only run at the end of the play. If you use --tags that exclude the handler's tag, it won't run. Always verify with --list-tasks.
restart app that was untagged. When we ran --tags deploy, the handler never ran, causing the app to use old config. Now all handlers have tags: always.always or the same tags as notifying tasks.Using --list-tags and --list-tasks for Debugging
These flags are your best friends for debugging tag issues. --list-tags shows all tags used in the playbook. --list-tasks shows tasks that will run, optionally filtered by --tags.
Example:
``bash ansible-playbook playbook.yml --list-tags ansible-playbook playbook.yml --list-tasks --tags deploy ``
The second command shows only tasks tagged with deploy. If a task you expect is missing, check its tag.
Production insight: I always run --list-tasks --tags mytag before a production deploy to confirm exactly what will execute. This caught many issues, like a new task accidentally tagged with never.
--list-tasks --tags <your_tags> as a dry run to verify the scope.tags: deply (typo). Our CI used --tags deploy, so the task never ran. We only caught it because --list-tags showed deply as a separate tag.--list-tags and --list-tasks to validate tag usage before running.Common Pitfall: Tags and `--check` Mode
When using --check mode, tags still apply. So ansible-playbook playbook.yml --check --tags deploy will only check tasks tagged with deploy. This is useful for dry-running a subset.
However, --check does not run handlers. So if you're testing a handler tag, you won't see it in check mode. Use --list-tasks instead.
Production insight: We use --check --tags deploy in CI to validate syntax and task selection before actual deployment.
--check mode; use --list-tasks to see handler tags.--check but failed in real run because a handler was missing tags. Now we always run --list-tasks to verify handlers.--check with --tags for safe previews; remember handlers are not executed in check mode.Tagging with `ansible-navigator`
If you use ansible-navigator (the TUI for Ansible), tags work the same way. You can pass --tags and --skip-tags as CLI arguments. In ansible-navigator's interactive mode, you can filter by tags using the :tags command.
Example:
``bash ansible-navigator run playbook.yml --tags deploy ``
Inside navigator, press : and type tags deploy to see only tagged tasks.
:tags deploy to filter the task list.:tags to filter interactively.Tagging and Ansible Vault
Tags do not affect vault decryption. If you have vault-encrypted variables, they are decrypted regardless of tags. However, tasks that use those variables will only run if tagged appropriately.
Gotcha: If you run --tags deploy and a task with vault variables is not tagged deploy, it won't run, but the vault password will still be required because Ansible parses the entire playbook.
--tags deploy still asked for the vault password, which confused new team members. We documented this behavior.Advanced: Dynamic Tags with Variables
You can set tags using variables, but be careful: tags are evaluated at parse time for static imports, but dynamically for includes. This can lead to unexpected behavior.
Example:
``yaml - name: Run with dynamic tags include_tasks: somefile.yml tags: "{{ my_tags }}" ``
If my_tags is a list, it works. But if the variable is undefined, the task will fail. Use to provide a fallback.default()
Production insight: We avoid dynamic tags in static imports because they cause parse-time errors. For dynamic includes, we use them sparingly.
The 'always' Tag That Never Ran
tags: never which was skipped because --tags deploy excluded it.tags: always so it runs regardless of tag filters.- Always explicitly tag handlers that must run on notifications, or use
alwaystag for critical actions.
--tags filter is too restrictive. Run ansible-playbook playbook.yml --list-tasks --tags yourtag to see matched tasks. If none, the tag is misspelled or not applied.--tags filter, or use tags: always on the handler.tags: always are skippedalways only runs when --tags is explicitly used; if you use --skip-tags always, it will be skipped. Verify you didn't accidentally skip it.tags: foo, all tasks inside get foo. But if you also have --tags bar, only tasks with bar run. Use --tags foo,bar to include both.ansible-playbook playbook.yml --list-tagsansible-playbook playbook.yml --list-tasks --tags mytagKey takeaways
--list-tags and --list-tasks --tags to preview before running.always tag ensures tasks run even with --tags; never excludes them unless explicitly included.apply replaces, not adds.--check mode; use --list-tasks to verify.--list-tags.--check with --tags for safe dry runs.Common mistakes to avoid
6 patternsNot tagging handlers
tags: alwaysUsing --skip-tags always
Typo in tag name
Assuming tags on block apply to handlers
Using apply: tags: to add instead of replace
Not using --list-tasks before run
Interview Questions on This Topic
What is the difference between --tags and --skip-tags?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Ansible. Mark it forged?
7 min read · try the examples if you haven't