An early milestone for an Ansible developer is when you first begin mixing your traditional playbooks with roles. As you’ll see in this post, one of the most effective ways to get the many benefits of roles, without sacrificing the straightforwardness of your playbooks– especially in terms of being precise about what tasks you actually want to run at any given time– is by using Ansible tags.
Upfront, Ansible playbooks and roles are interrelated concepts, but a helpful distinction is that, in practice at least, a basic playbook (e.g., ‘install-web-server.yml’) typically declares what you want DONE to a node; while a role declares what a node like a “web server” actually IS in your environment. What, exactly, makes a web server different from a database server? Whatever it is, we can define those unique things in an Ansible role named “web-server.” (And as a bonus, another team in the company can subsequently discover and use our web-server role to do the same for themselves, and probably more easily than they could your playbook!)
And it’s when we can start to consistently describe our infrastructure in terms of bigger-picture concepts like server roles, instead of merely using Ansible playbooks to “do” things to our servers, that we really start going places on our automation journey.
However! When you begin refactoring your original Ansible playbook, you may end up facing some decision points. Let’s say for instance that you’ve only ever run this particular playbook when you are provisioning a new web server, because while it performs 10 tasks, with maybe 3 of those tasks, you wouldn’t want to run them again (even though Ansible is declarative and idempotent, this still happens sometimes IRL. We get it).
Another potential decision point that follows is around those other 7 Ansible tasks. Those tasks might be related to monthly patching, periodic application deployments, occasional changes to config files, and so on. You might even already be duplicating those tasks in one or more other playbooks– which technically goes against the DRY (Don’t Repeat Yourself) Principle. Or you might not have yet been able to automate those tasks anywhere else except in this playbook we’re presently seeking to convert into a role, and instead you’re still doing those (gulp) manually.
(Notably, in recent years in software development circles, the classic DRY Principle has been challenged by the DAMP (“Descriptive And Meaningful Phrases”) Principle. But for IaC code like Ansible, Level Up generally still recommends DRY.)
So what to do? The simplest way to get the best of both worlds (and possibly even be able to git rm
an extra legacy playbook into oblivion in the process) is to start using tags to separate the “provision” tasks from the “update” tasks. Here’s what that might look like (and to keep it super simple, we’re swapping actual automation tasks for debug messages, but we hope the essential ideas still come across.
- Given a sparse file layout like this:
$ tree
.
├── roles
│ └── web-server
│ └── tasks
│ └── main.yml
└── site.yml
4 directories, 2 files
$
2. Given a classic “site.yml” playbook to trigger the role like this:
# site.yml:
---
- name: "Test web-server role"
hosts: localhost
gather_facts: false
roles:
- role: roles/web-server
3. Given a tasks/main.yml file like this:
# roles/web-server/tasks/main.yml:
---
# tasks file for web-server
- name: "Task 1: runs only on provision"
ansible.builtin.debug:
msg: "Task 1 of the web-server role"
tags:
- provision
- name: "Task 2: runs on provision and update"
ansible.builtin.debug:
msg: "Task 2 of the web-server role"
tags:
- provision
- update
- name: "Debug update with never"
ansible.builtin.debug:
msg: "update with never tag test from web-server role"
tags:
- update
- never
- name: "Debug always"
ansible.builtin.debug:
msg: "always tag test from web-server role"
tags:
- always
- name: "Debug never"
ansible.builtin.debug:
msg: "never tag test from web-server role"
tags:
- never
- name: "Debug untagged"
ansible.builtin.debug:
msg: "untagged tag test from web-server role"
4. You should be able to test these usage examples and see how the tags behave in different combinations, etc.
# Usage examples:
$ ansible-playbook site.yml # runs all tasks except the tasks with the 'never' tag
$ ansible-playbook site.yml --tags provision # AND always
$ ansible-playbook site.yml --tags update # AND always
$ ansible-playbook site.yml --tags provision,update # AND always
$ ansible-playbook site.yml --tags never # runs never AND always
$ ansible-playbook site.yml --tags never --skip-tags always # runs never only
$ ansible-playbook site.yml --tags untagged # AND always
And if you actually go through these ansible-playbook examples one by one and study the outputs, you’ll notice three tags that we’ve included which are special tags: always; never; and untagged. These special tags reveal an extra layer of “default” behaviors which are available to you out of the box, and which could be useful depending on the automation scenario.
In conclusion, Ansible tags are an extremely flexible way to precisely define any subset of tasks you want to automate, from job run to job run, and regardless of whether those tasks are defined in a playbook or a role. And especially when it comes to Ansible roles, tags provide an instant opportunity for you to begin predictably (and therefore safely) maintaining and executing all of the code that makes a node under one roof, regardless of whether it’s Day 1 or Day 1,001 in that node’s lifecycle.