As a frequent Ansible user, I'm always looking at ways to simplify my playbooks and save time during playbook debugging. One of my favorite features for writing robust Ansible playbooks is its support for tags. This article introduces tags, walks through some common tag scenarios, and outlines more advanced usage.
What are tags?
Tags are metadata that you can attach to the tasks in an Ansible playbook. They allow you to selectively target certain tasks at runtime, telling Ansible to run (or not run) certain tasks. While you would typically run an entire Ansible playbook from start to finish, it can be extremely useful to run specific tasks within a playbook on demand.
As an example, the playbook below defines two tags: hello
and goodbye
. You can use these tags to control how Ansible runs the playbook:
---
- hosts: localhost
gather_facts: False
tasks:
- name: Hello tag example
debug:
msg: "Hello!"
tags:
- hello
- name: No tag example
debug:
msg: "How are you?"
- name: Goodbye tag example
debug:
msg: "Goodbye!"
tags:
- goodbye
You can tell Ansible to run only the tags with the -t
or --tags
flag:
$ ansible-playbook basic_example.yml --tags hello
PLAY [localhost]
*******************************************
TASK [Hello tag example]
*******************************************
ok: [localhost] => {
"msg": "Hello!"
}
PLAY RECAP
*******************************************
localhost: ok=1 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
You can also tell Ansible to skip tasks with certain tags by using the --skip-tags
flag:
$ ansible-playbook basic_example.yml --skip-tags goodbye
PLAY [localhost]
*******************************************
TASK [Hello tag example]
*******************************************
ok: [localhost] => {
"msg": "Hello!"
}
TASK [No tag example]
*******************************************
ok: [localhost] => {
"msg": "How are you?"
}
PLAY RECAP
*******************************************
localhost: ok=2 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
Finally, you can list the tags in your playbook by using the --list-tags
flag:
$ ansible-playbook basic_example.yml --list-tags
playbook: basic_example.yml
play #1 (localhost): localhost TAGS: []
TASK TAGS: [goodbye, hello]
Ensuring that a task always (or never) runs
As you use tags in your playbooks, you will find that sometimes you have tasks that you always want to run, even when using --tags
or --skip-tags
. For example, imagine a playbook that performs the installation, configuration, and verification of your company's software. You have a task that obtains the latest version of the app to install by making a request to a web endpoint. You use tags to distinguish each step, in case you want to run a certain part of the playbook to save time. However, you wish for the software version task to always run, as it contains information that is relevant to all of the other tasks. If a user targets only tasks tagged with configure
, the playbook still needs to run the software version task to obtain critical information. You can accomplish this with the always
tag:
---
- hosts: appservers
gather_facts: False
tasks:
- name: Obtain latest software version from API
uri:
url: "http://config-server.example.com:8080/api/v1/appVersion"
register: app_version
delegate_to: localhost
tags:
- always
- name: Install software
package:
name: "{{ app_version.json.version }}"
state: present
tags:
- install
- name: Configure software
template:
src: templates/app_config.json.j2
dest: /etc/app/app_config.json
mode: 0644
owner: root
group: root
tags:
- configure
- name: Verify software
command: /usr/local/sbin/test_app.sh {{ app_version.json.version }}
tags:
- verify
When no tags are passed, the entire playbook runs, as expected:
$ ansible-playbook -i inventory.ini always_runs.yml
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Install software]
*************************************************************
ok: [appserver.example.com]
TASK [Configure software]
*************************************************************
ok: [appserver.example.com]
TASK [Verify software]
*************************************************************
changed: [appserver.example.com]
PLAY RECAP
*************************************************************
appserver.example.com : ok=4 changed=1 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
When the configure
tag is passed, the configure task runs, as expected. The software version task also runs because it has the always
tag:
$ ansible-playbook -i inventory.ini always_runs.yml -t configure
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Configure software]
*************************************************************
ok: [appserver.example.com]
PLAY RECAP
*************************************************************
appserver.example.com : ok=2 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
Ansible also provides the never
tag, which does the opposite of always
. It ensures that a task will never run unless the user specifically targets it. Imagine that you don't want the verification task to run unless the playbook user specifically calls for it. The never
tag solves this problem:
- name: Verify software
command: /usr/local/sbin/test_app.sh {{ app_version.json.version }}
tags:
- verify
- never
Even though no tags are specified, the verification task doesn't run because it has the never
tag:
$ ansible-playbook -i inventory.ini always_runs.yml
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Install software]
*************************************************************
ok: [appserver.example.com]
TASK [Configure software]
*************************************************************
ok: [appserver.example.com]
PLAY RECAP
*************************************************************
appserver.example.com : ok=3 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
A tagged task still runs if it is explicitly called:
$ ansible-playbook -i inventory.ini always_runs.yml -t verify
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Verify software]
*************************************************************
changed: [appserver.example.com]
PLAY RECAP
*************************************************************
appserver.example.com : ok=2 changed=1 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
Tag inheritance
Writing a single, monolithic playbook is rare. You likely have playbooks that import other playbooks, include tasks from other files, or use a role structure to organize your code. Understanding how tags are passed within this hierarchy of imports is important because tags become tricky when you use includes or imports.
[ Learn more by signing up for the free webinar Ansible 101: An introduction to automating everything with training. ]
Ansible differentiates between static imports and dynamic includes. When using an import (such as import_tasks
), Ansible also applies related tags to all of the imported tasks. For example, consider the following code, which refactors the single playbook from the last section into two YAML files: playbook.yml
and install.yml
. (I've omitted the configuration and validation tasks for brevity.) The main playbook.yml
uses import_tasks
to pull in the tasks in the install.yml
file:
# playbook.yml
---
- hosts: appservers
gather_facts: False
tasks:
- name: Obtain latest software version from API
uri:
url: "http://config-server.example.com:8080/api/v1/appVersion"
register: app_version
delegate_to: localhost
tags:
- always
- import_tasks: install.yml
tags:
- install
# install.yml
---
- name: Install software
package:
name: "{{ app_version.json.version }}"
state: present
Here's the playbook output:
$ ansible-playbook -i inventory.ini playbook.yml -t install
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Install software]
*************************************************************
ok: [appserver.example.com]
PLAY RECAP
*************************************************************
appserver.example.com : ok=2 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
Notice how the task in install.yml
ran despite not having the install
tag. This occurs because the install
tag applied in the main playbook.yml
is inherited by all tasks imported by import_tasks
. However, if I change import_tasks
to include_tasks
, I get very different behavior:
# playbook.yml
---
- hosts: appservers
gather_facts: False
tasks:
- name: Obtain latest software version from API
uri:
url: "http://config-server.example.com:8080/api/v1/appVersion"
register: app_version
delegate_to: localhost
tags:
- always
- include_tasks: install.yml
tags:
- install
The output:
$ ansible-playbook -i inventory.ini playbook.yml -t install
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [include_tasks]
*************************************************************
included:
/home/[...]/ansible_tags/dynamic_imports/install.yml
for appserver.example.com
PLAY RECAP
*************************************************************
appserver.example.com : ok=2 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
Notice the install
task no longer runs. When a dynamic include_task
is used, the tags apply only to the include
itself. The tasks within the include
do not inherit the tags, so they must also be defined on the included tasks:
# install.yml
---
- name: Install software
package:
name: "{{ app_version.json.version }}"
state: present
tags:
- install
Manually adding tags to every task within an included file can be tedious and error-prone, so Ansible uses the apply
keyword to assist with this. See the documentation for more information.
Using tags to simplify playbook debugging
A great way to appreciate the usefulness of tags is to leverage them when writing and debugging playbooks. In the example below, Ansible throws an error about an undefined variable at the install task:
TASK [Install software]
*********************************
fatal: [appserver.example.com]: FAILED! =>
{"msg": "The task includes an option with an undefined variable.
The error was: 'dict object' has no attribute 'version'\n\nThe error
appears to be in '/home/ansible/debug_tags.yml': line 21, column 7,
but may\nbe elsewhere in the file depending on the exact syntax
problem.\n\nThe offending line appears to be:\n\n\n
- name: Install software\n
^ here\n"}
So I add debug
tags to the tasks that I want to run to aid in troubleshooting. In this case, I added a debug task to print out the registered variable, and I also added a debug
tag to the install task. This allows me to only run specific tasks while troubleshooting, which saves time since I don't have to run the entire playbook:
---
- hosts: appservers
gather_facts: False
tasks:
- name: Obtain latest software version from API
uri:
url: "http://config-server.example.com:8080/api/v1/appVersion"
register: app_version
delegate_to: localhost
tags:
- always
- name: Print out software version API response
debug:
msg: "{{ app_version }}"
tags:
- debug
- name: Install software
package:
name: "{{ app_version.json.version }}"
state: present
tags:
- install
Using this debug logic quickly shows that I'm referencing the wrong dictionary key (appversion.json.version
instead of appversion.json.softwareVersion
):
$ ansible-playbook -i inventory.ini debug_tags.yml -t debug
PLAY [appservers]
*************************************************************
TASK [Obtain latest software version from API]
*************************************************************
ok: [appserver.example.com -> localhost]
TASK [Print out software version API response]
*************************************************************
ok: [appserver.example.com] => {
"msg": {
"changed": false,
"connection": "close",
"content_length": "51",
"content_type": "application/json; charset=utf-8",
"cookies": {},
"cookies_string": "",
"date": "Thu, 14 Oct 2021 17:00:41 GMT",
"elapsed": 0,
"failed": false,
"json": {
"softwareVersion": "my-app=7.68.0-1ubuntu2.7"
},
"msg": "OK (51 bytes)",
"redirected": false,
"status": 200,
"url": "http://config-server.example.com:8080/api/v1/appVersion"
}
}
PLAY RECAP
*************************************************************
appserver.example.com : ok=2 changed=0 unreachable=0
failed=0 skipped=0 rescued=0 ignored=0
I usually remove these debug tags once my playbook is working and before committing it to source control. However, as in the previous section, you could also leave additional debug tasks in your playbooks and just tag them with the never
tag.
Wrapping up
Finding ways to better structure and run your Ansible playbooks is critical to developing a flexible automation strategy. Ansible offers many different ways to accomplish this; imports, includes, roles, and other patterns allow you to write robust and reusable code.
Tags provide a quick way to target specific Ansible tasks. Knowing how to use tags properly, leverage always
and never
tags, and understand the nuances of tag inheritance will level up your Ansible expertise and allow you to design better automation.
About the author
Anthony Critelli is a Linux systems engineer with interests in automation, containerization, tracing, and performance. He started his professional career as a network engineer and eventually made the switch to the Linux systems side of IT. He holds a B.S. and an M.S. from the Rochester Institute of Technology.
Browse by channel
Automation
The latest on IT automation for tech, teams, and environments
Artificial intelligence
Updates on the platforms that free customers to run AI workloads anywhere
Open hybrid cloud
Explore how we build a more flexible future with hybrid cloud
Security
The latest on how we reduce risks across environments and technologies
Edge computing
Updates on the platforms that simplify operations at the edge
Infrastructure
The latest on the world’s leading enterprise Linux platform
Applications
Inside our solutions to the toughest application challenges
Original shows
Entertaining stories from the makers and leaders in enterprise tech