Skip to main content

Use Ansible tags to save time on playbook runs

Instead of running an entire Ansible playbook, use tags to target the specific tasks you need to run.
Image
Blank gift tags

Photo by Miguel Á. Padriñán from Pexels

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.

Topics:   Ansible   Troubleshooting  
Author’s photo

Anthony Critelli

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. More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.