Re-use artifacts (playbooks, tasks, ...) in Ansible

Posted on April 15, 2021 by Adrian Wyssmann ‐ 4 min read

I recently struggled into an issue where I have certain tasks which I want to run for the bootstrapping, as well when applying the desired state. I don't want to have to repeat the same task in different playbooks, so I had a look into reusable of ansible elements

What does Ansible say about re-using artifacts

Re-using Ansible artifacts describes it pretty well

You can write a simple playbook in one very large file, and most users learn the one-file approach first. However, breaking tasks up into different files is an excellent way to organize complex sets of tasks and reuse them.

You can have different artifacts which can be re-used

  • variables file contains only variables.
  • task file contains only tasks.
  • playbook contains at least one play, and may contain variables, tasks, and other content. You can re-use tightly focused playbooks, but you can only re-use them statically, not dynamically.
  • role contains a set of related tasks, variables, defaults, handlers, and even modules or other plugins in a defined file-tree, see Ansible Roles

Known the different artifacts you also shall understand how you can use them. Ansible supports Including and importing. This means you can do various things:

  • Import a task list: Imports a list of tasks to be added to the current playbook for subsequent execution.
  • Load and execute a role: Dynamically loads and executes a specified role as a task, but has some constraints (check the docu)
  • Import a role into a play: Much like the roles: keyword, this task loads a role, but it allows you to control when the role tasks run in between other tasks of the play.
  • Import a playbook: Includes a file with a list of plays to be executed, but cannot be used inside a play. If it includes other plays, it can only be included at the top level.

What is my issue

I heavily rely on roles when working with ansible, but still if you want to re-apply the same set of roles in different plays you may still need to use other artifacts. This is exactly the case for me, so when I bootstrap my servers, I want to apply the same roles - e.g. base-system, papanito.cloudflared, oefenweb.fail2ban, etc. However I also want to apply the same roles as for my site.yml.

As briefly mentioned in this post I have a bootstrap playbook which in does create the servers and applies the generic roles.

...
- hosts: "{{ server_names }}"
  vars:
    ansible_host: "{{ server_ipv4 }}"
    ansible_ssh_user: "{{ bootstrap_remote_user }}"
  tags:
    - hcloud-server

  roles:
  - role: base-system
    vars:
      ansible_host: "{{ server_ipv4 }}"
  - role: papanito.cloudflared
  - role: networking
  - role: oefenweb.fail2ban
  - role: papanito.rsyslog

I have also a production.yml which also does apply th same roles - with a slight difference that the ansible_host and ansible_ssh_user are different, as well as the hosts.

- name: Apply generic server roles to properly integrate with the infrastructure setup
  hosts: all
  become: yes

  roles:
    - role: base-system
    - role: papanito.cloudflared
    - role: networking
    - role: oefenweb.fail2ban
    - role: papanito.rsyslog

This is necessary due to the way I have chosen to do my setup:

  • add specific user for ansible, disable login for root
  • install fail2ban
  • make an ssh hardening
  • setup a cloudflare argo tunnel so I can access the server via xxx.example.com and have additional protection (zero trust policy)

The issue is, whenever there is a new “generic” role to be applied I have to add it in more than one playbook - not a big deal if you only have few playbooks - but still I don’t like to have to repeat things which tend to get forgotten.

So what now?

For my use cases I created a playbook generic_roles.yml which contains all roles I want to apply for all my nodes:

- name:  Apply generic server roles to properly integrate with the infrastructure setup
  hosts: "{{ target }}"
  become: yes

  roles:
    - role: base-system # create required user and install required packages
      tags:
        - base
    - role: networking # set dns and stuff
      tags:
        - networking
    - role: oefenweb.fail2ban # disable root access
      tags:
        - networking
        - hardening
    - role: papanito.cloudflared # no direct access, use argo tunnel
      tags:
        - networking
        - hardening
    - role: papanito.rsyslog # Logs will be forwarded to an external service
      tags:
        - monitoring
        - logging

The hosts is not fixed but excepts a target variable. Then I call this playbook as follows in my bootstrap_hcloud_servers

- name: Apply generic server roles for {{ server_names }}
  import_playbook: generic_roles.yml
  vars:
    target: "{{ server_names }}"
    ansible_host: "{{ server_ipv4 }}"
    ansible_ssh_user: "{{ bootstrap_remote_user }}"
  tags:
    - hcloud-server

The same playbook can be use in the site.yml

- name: Apply generic server roles for all servers
  import_playbook: generic_roles.yml
  vars:
    target: "all"

So my simplified site.yml would look like this:

- name: Apply generic server roles for all servers
  import_playbook: roles_generic.yml
  vars:
    target: "all"

- name: Apply rss server roles for rss servers
  import_playbook: roles_ttrss.yml

This way I can always run site.yml to ensure the desired state is applied to all my nodes, but also I could run roles.ttrss.yml alone in case I only need to apply the state to my rss hosts.

That’s it, mission accomplished. You see Ansible has a lot of interesting concepts, which helps you to properly structure your project so that you don’t have to multiply the same “code” in various places.