Ansible – Proper method for handling roles, tasks, dependencies

ansibleansible-playbook

Ansible – Proper method for handling roles, tasks, dependencies ?

Related to How to keep ansible role from running multiple times when listed as a dependency?

This is a long post/question. TL;DR – what's the right way to setup tagging of roles and tasks such that dependencies will work correctly without roles being run multiple times.

See Ansible-test-deps for a playbook setup to illustrate the issues.

I've been having some trouble with getting playbooks to play well with tags and dependencies. In general, I want a playbook with a bunch of roles, (each with some set of dependencies) to "work cleanly". This alone is fairly easy to set up, and works well when using all the roles in the whole playbook. The roles with dependencies can be defined in any order in the playbook, and those dependencies ensure that they're run in the correct order. Of course roles without dependencies will run in the order they appear in the playbook roles: section.

But there are times when one wants to just run a subset of the roles, and then it falls apart, with some roles being run multiple times, and in weird orders.

So I've built a test setup, with 4 roles (A B C D), and a playbook with multiple tagging methods used. Actually, it's 4 roles with bare untagged tasks, and 4 roles with tagged tasks … plus a role named 'z' with no tasks, just a dependency for all the other roles. They look like this :

Role name    Dependencies
----------   ------------
a_tagged     none
b_tagged     a_tagged
c_tagged     b_tagged
d_tagged     c_tagged b_tagged a_tagged
z_tagged     a_tagged b_tagged c_tagged d_tagged
a_untagged   none
b_untagged   a_untagged
c_untagged   b_untagged
d_untagged   c_untagged b_untagged a_untagged
z_untagged   a_untagged b_untagged c_untagged d_untagged

I know it looks cumbersome, but we're trying to test basically 4 combinations :

Role config         Tasks config
-----------         ------------
Role IS tagged      Tasks are NOT tagged
Role IS tagged      Tasks ARE tagged
Role is NOT tagged  Tasks are NOT tagged
Role is NOT tagged  Tasks ARE tagged

Each role tasks/main.yml looks like this – in this example role "b_tagged" has just one task, and that task is tagged with "tags: b"

./roles/b_tagged/tasks/main.yml

---
- debug: msg="Role B tagged"
  tags: b

and the associated meta/main.yml like this, so role b_tagged depends on role a_tagged :

./roles/b_tagged/meta/main.yml

---
dependencies:
  - { role: a_tagged }

The corresponding b_untagged style tasks are identical, but without the "tags:" line in the task.

The desired result is for the roles to execute in the order A B C D and each one runs only once, like this :

    "msg": "Role A ..."
    "msg": "Role B ..."
    "msg": "Role C ..."
    "msg": "Role D ..."

To provide for the minimal output only showing the debug msg: output, the script "test.sh" just runs ansible-playbook as follows. Wherever you see test.sh it's simply running this command.

ANSIBLE_STDOUT_CALLBACK=minimal ansible-playbook -i hosts test.yml $@ -- | egrep -v "{|}"

There are 4 main scenarios for running the plays …

  • Run every role in each play, default typical use
    ./test.sh

  • Run every role in each play, using the 'z' tag to select every role/task.
    Remember, the 'z' role just has all the other roles as dependencies
    ./test.sh --tags 'z'

  • Run each play for only the 'c' tagged roles/tasks
    this should only run roles A B C
    ./test.sh --tags 'c'

  • Run each play for only the 'b' and 'c' tagged roles/tasks
    this should only run roles A B C
    ./test.sh --tags 'b,c'

Each play in the playbook file test.yml is of this form : (this is the 1st one)

./test.yml

###########################################################################################
# roles with NO tags and tasks WITH tags, roles defined in reverse order from dependencies.
- hosts: localhost
  gather_facts: false
  become: no

  pre_tasks:
    - debug:
        msg: "=============== untagged roles z d c b a tagged tasks reverse ========"
      tags: always

  roles:
    - role: z_tagged
    - role: d_tagged
    - role: c_tagged
    - role: b_tagged
    - role: a_tagged
###########################################################################################

Since only the role definitions change for each playbook (and the msg: string) that's all that's listed here for the other plays in the test.yml playbook file

* roles with NO tags and tasks with NO tags, roles defined in reverse order from dependencies
    - role: z_untagged
    - role: d_untagged
    - role: c_untagged
    - role: b_untagged
    - role: a_untagged

* roles WITH tags and tasks WITH tags, roles defined in correct order from dependencies
    - { role: a_tagged, tags: a }
    - { role: b_tagged, tags: b }
    - { role: c_tagged, tags: c }
    - { role: d_tagged, tags: d }
    - { role: z_tagged, tags: z }

* roles WITH tags and tasks WITH tags, roles defined in reverse order from dependencies
    - { role: z_tagged, tags: z }
    - { role: d_tagged, tags: d }
    - { role: c_tagged, tags: c }
    - { role: b_tagged, tags: b }
    - { role: a_tagged, tags: a }

* roles WITH tags and tasks with NO tags, roles defined in correct order from dependencies
    - { role: a_untagged, tags: a }
    - { role: b_untagged, tags: b }
    - { role: c_untagged, tags: c }
    - { role: d_untagged, tags: d }
    - { role: z_untagged, tags: z }

* roles WITH tags and tasks with NO tags, roles defined in reverse order from dependencies
    - { role: z_untagged, tags: z }
    - { role: d_untagged, tags: d }
    - { role: c_untagged, tags: c }
    - { role: b_untagged, tags: b }
    - { role: a_untagged, tags: a }

Running the 4 scenarios above produces the following output.

./test.sh
Regardless of whether the individual tasks are tagged or not, (so long as the roles do not have tags) results in the correct output (first two plays). If the roles DO have tags, then the roles are run multiple times (as seen in the next 4 plays), the order depending on the order in which they are defined in the play.

Correct example - role: a_tagged or - role: a_untagged order of roles does not matter

"msg": "=============== untagged roles z d c b a tagged tasks reverse ========"
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role D tagged"
"msg": "=============== untagged roles z d c b a untagged tasks reverse ======"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "Role D untagged"
"msg": "=============== tagged roles a b c d z tagged tasks =================="
"msg": "Role A tagged"
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role C tagged"
"msg": "Role D tagged"
"msg": "Role D tagged"
"msg": "=============== tagged roles z d c b a tagged tasks reverse =========="
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role D tagged"
"msg": "Role D tagged"
"msg": "Role C tagged"
"msg": "Role B tagged"
"msg": "Role A tagged"
"msg": "=============== tagged roles a b c d z untagged tasks ================"
"msg": "Role A untagged"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "Role C untagged"
"msg": "Role D untagged"
"msg": "Role D untagged"
"msg": "=============== tagged roles z d c b a untagged tasks reverse ========"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "Role D untagged"
"msg": "Role D untagged"
"msg": "Role C untagged"
"msg": "Role B untagged"
"msg": "Role A untagged"

./test.sh --tags 'z'
When using the 'z' tag to select all the roles/tasks, only the plays with roles that had tags: in their definition produced the correct output. If the role wasn't tagged, then the tasks in that role never execute, regardless of whether they're tagged or not.

Correct example - { role: a_tagged, tags: a } or - { role: a_untagged, tags: a } order of roles does not matter

"msg": "=============== untagged roles z d c b a tagged tasks reverse ========"
"msg": "=============== untagged roles z d c b a untagged tasks reverse ======"
"msg": "=============== tagged roles a b c d z tagged tasks =================="
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role D tagged"
"msg": "=============== tagged roles z d c b a tagged tasks reverse =========="
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role D tagged"
"msg": "=============== tagged roles a b c d z untagged tasks ================"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "Role D untagged"
"msg": "=============== tagged roles z d c b a untagged tasks reverse ========"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "Role D untagged"

./test.sh --tags 'c'
Running just a selected role (and its dependencies) the only correct output was from having the roles tagged and the tasks NOT tagged (last two plays).

Correct example - { role: a_untagged, tags: a } order of roles does not matter

"msg": "=============== untagged roles z d c b a tagged tasks reverse ========"
"msg": "Role C tagged"
"msg": "=============== untagged roles z d c b a untagged tasks reverse ======"
"msg": "=============== tagged roles a b c d z tagged tasks =================="
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "Role C tagged"
"msg": "=============== tagged roles z d c b a tagged tasks reverse =========="
"msg": "Role C tagged"
"msg": "Role A tagged"
"msg": "Role B tagged"
"msg": "Role C tagged"
"msg": "=============== tagged roles a b c d z untagged tasks ================"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"
"msg": "=============== tagged roles z d c b a untagged tasks reverse ========"
"msg": "Role A untagged"
"msg": "Role B untagged"
"msg": "Role C untagged"

./test.sh --tags 'b,c'
Running just two selected roles (and their dependencies) there were NO correct outputs.

NOTE: NONE of the configurations produce the required result of running just roles "A B C"

    "msg": "=============== untagged roles z d c b a tagged tasks reverse ========"
    "msg": "Role B tagged"
    "msg": "Role C tagged"
    "msg": "=============== untagged roles z d c b a untagged tasks reverse ======"
    "msg": "=============== tagged roles a b c d z tagged tasks =================="
    "msg": "Role A tagged"
    "msg": "Role B tagged"
    "msg": "Role B tagged"
    "msg": "Role C tagged"
    "msg": "Role C tagged"
    "msg": "=============== tagged roles z d c b a tagged tasks reverse =========="
    "msg": "Role B tagged"
    "msg": "Role C tagged"
    "msg": "Role A tagged"
    "msg": "Role C tagged"
    "msg": "Role B tagged"
    "msg": "=============== tagged roles a b c d z untagged tasks ================"
    "msg": "Role A untagged"
    "msg": "Role B untagged"
    "msg": "Role B untagged"
    "msg": "Role C untagged"
    "msg": "=============== tagged roles z d c b a untagged tasks reverse ========"
    "msg": "Role A untagged"
    "msg": "Role B untagged"
    "msg": "Role C untagged"
    "msg": "Role B untagged"

Conclusion … the only way to be able to handle the two scenarios with dependencies (run the whole playbook (all roles) or run just a subset of roles) is to ensure that the tags used for selection are ONLY in the role definition, and NOT on the tasks themselves. Such as :

    - { role: a_untagged, tags: a }

Any tags on the tasks should only be for the tasks, NOT for any role selection. Even so, this only works when selecting a single role/tag via –tags 'c' to run, and fails with multiples via –tags 'b,c' as the last sample shows.

In the test.yml playbook file, that's the last two plays (role order doesn't matter). All other variants do not produce the correct results. It almost seems as if there's no reason to have tags: on the tasks themselves, at least if you want role/task selection to work with dependencies and no multiple-executions.

Best Answer

I don't have a precise answer for you, but "stop holding Ansible like a puppet" ;-)

I can imagine the need for such dependencies, but you also might want to think about: Can I manage the dependencies on my own?

I've experienced nearly the same: A playbook that loads the roles logstash and tomcat. Both need the role java. Our Java-role had also a meta-dependency. So the role java would get executed twice on a host-rollout (and also the dependency of our java-role).

---
- hosts: myhost
  roles:
    - role: logstash
    - role: tomcat8

We solved this by doing it like this:

---
- hosts: myhost
  roles:
    - role: java
    - role: logstash
    - role: tomcat8

Remove the dependency of role java from the roles logstash and tomcat. This way java get's rolled out only once in a playbook-run.

This solution might helpful when you know and control your tools on your own and want to keep it simple, not complex.