18
votes

On the Ansible best practices page: http://docs.ansible.com/ansible/playbooks_best_practices.html#top-level-playbooks-are-separated-by-role it shows an example where the master playbook site.yml includes a couple of other top-level playbooks webservers.yml and dbservers.yml. Within those playbooks they each include the common role. Some inventory files I have all my groups run on one single host. Another inventory file I have a host per group. For the case where ever group is on one host, if I run site.yml you can see that the common role is getting played twice, one for webservers.yml and one for dbservers.yml.

What is a solution to avoid this? I guess you can take out the common role from webservers.yml and dbservers.yml and instead within site.yml have a task that targets both with the common role. But then I can not individually provision a webserver or dbserver with common.

7
What do you mean by "Some inventory files I have all my groups run on one single host. Another inventory file I have a host per group."?alex

7 Answers

7
votes

I determine role dependencies using meta files in my role directory. Role dependencies allow you to automatically pull in other roles when using a role. Role dependencies are stored in the meta/main.yml file contained within the role directory.

Roles dependencies are always executed before the role that includes them, and are recursive. By default, roles can also only be added as a dependency once - if another role also lists it as a dependency it will not be run again. This behavior can be overridden by adding allow_duplicates: yes to the meta/main.yml file.

See an example in the Ansible documentation.

3
votes

As others I had this problem, and even if the role is idempotent, it take time to execute, so we might want to execute it only once.

I used a similar approach than @Vor but I choose facts instead of creating files.

Reminder:

Facts survive between plays during an Ansible run, but will not be saved across executions even if you use a fact cache.

Example

site.yml

---
- hosts: all
  roles: [common]
- include: db.yml
- include: web.yml

db.yml

---
- hosts: db
  roles:
    - { role: common, when: isdef_common_role is not defined }
    - db

web.yml

---
- hosts: web
  roles:
    - { role: common, when: isdef_common_role is not defined }
    - web

roles/common/tasks/main.yml

---
- debug: 'msg="{{ inventory_hostname }} common"'
- set_fact: isdef_common_role=1

This is, indeed, a bit redundant (as you have to make a conditional include every time), but have the following advantages:

  • works on most situations I can think (ansible-playbook site.yml, ansible-playbook site.yml -l common, ansible-playbook site.yml -l db, ansible-playbook db.yml, ...)
  • let you decide if you want to repeat the execution of the common

If you do not want to repeat the { role: common, when: isdef_common_role is not defined }, you could put condition into the common role using the following:

site.yml: no changes

db.yml, web.yml: remove the conditional from the roles

roles/common/tasks/main.yml

---
- include: tasks.yml
  when: isdef_common_role is not defined
- set_fact: isdef_common_role=1

roles/common/tasks/tasks.yml

---
- debug: 'msg="{{ inventory_hostname }} common"'
3
votes

@user1087973 Are you using tags ? If you are using tags, a common role with different tags is consider as different :

For example :

role_common

role1 : host SRV tag_role_1 depends on role_common

role 2 : host SRV tag_role_2 depends on role_common

Your common role will be executed twice, because it is considered as different because of the tags

Take a look with --list-tasks and you will see that

A proper solution instead of using tags is to use different yml files and include it in your site.yml

http://docs.ansible.com/ansible/playbooks_best_practices.html#top-level-playbooks-are-separated-by-role

Hope this can help

2
votes

My approach is to not include playbooks in playbooks. At least not when doing so would result in a role executing multiple times in a single job.

Anything I need to include in more than 1 playbook gets converted into a role, and that role can be included in many playbooks. If I end up with several playbooks that include a duplicated series of roles, I can combine those roles into a single role that just depends on the other roles to avoid that duplication.

1
votes

My approach is create a lock file on the server for each role. This works quite nicely.

For example I have a role called common that's how my tasks/main.yml looks like:

- name: check lock file exist
  stat: path=/tmp/ansible-common-role
  ignore_errors: true
  register: lock_file

- name: install apt packages
  apt: name={{ item }} state=present 
  with_items:
    - python-setuptools
  when: lock_file.stat.exists == false

.....
# other tasks with 
#    when: lock_file.stat.exists == false
.....

- name: Create lock file
  file: path=/tmp/ansible-common-role state=touch
  when: lock_file.stat.exists == false

As you can see on the example above, the playbook will skip all of the tasks if it is already ran.

0
votes

You could also build a temp group using add_host, something like:

1. target: webservers--> add hosts to group "common_roles"
2. target: dbservers --> add hosts to gruop "common_roles"
3. Target: common_roles --> execute whatever common roles you need
4. regular group by group execution.
0
votes

As stated in the Ansible Documentation:

Ansible will only allow a role to execute once, even if defined multiple times, if the parameters defined on the role are not different for each definition.

(...)

To make roles run more than once, there are two options:
1. Pass different parameters in each role definition.
2. Add allow_duplicates: true to the meta/main.yml file for the role.

Also consider the distinct aspects of dynamic vs. static content reuse: Including and Importing

Keep in mind that playbooks are run on a inventory item basis, and all that role schema and code reuse is meant to organize and ease maintenance of that infrastructure as code. And for each run, tasks are supposed to take effect on the same host, and produce idempotent results.

That said, it makes no sense at first to run the same group of tasks (i.e, a role) twice, for any subsequent execution would have no practical effect (due to idempotency).

The exception is when role invocations have distinct parameters (variables, tags, etc.), thus actual diferences on the execution, possibly affecting idempotency and leading to somewhat distinct results. On those cases, it makes sense to run the same group of tasks more than once, and that's what happens.

As a rule of thumb, specially concerning static content, all code that will be executed is like one single and self-contained playbook (even in a "site" playbook comprising two or more "child" playbooks), and scope is nevertheless global.