Roles & Collections
General Organization
We recommend to generally keep a strict separation between roles and collections on the one hand and playbook, inventory and host vars on the other hand. This makes it easier to reuse roles at a later time. Another advantage of this strict separation is that roles and collections can later on be published as open source without risking the exposure of sensitive customer data.
We also recommend to put each role or collection into its own Git repository. Again, this makes reuse easier.
We generally recommend to prefer collections over single-role repositories. While there is no official “don’t use single-role repos” announcement yet, and they won’t go away for a long time, they for example are not supported in Ansible Automation Hub.
Roles
The following recommendations apply to both roles within collections as well as single-role repositories.
A new role with all its boilerplate can be created using the command
ansible-galaxy role init <rolename>
. This can be used both within
the roles/
folder of a collection and for a standalone role
repository.
Role Directory Layout
.
├── defaults/
│ └── main.yml
├── files/
│ └── etc/
│ └── default/
│ └── ssh
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── tasks/
│ ├── configuration.yml
│ ├── installation.yml
│ └── main.yml
├── templates/
│ └── etc/
│ └── ssh/
│ └── sshd_config.j2
└── vars/
├── Debian.yml
└── RedHat.yml
Tasks
We use main.yml
only to import other YAML files and to assign tags to the imported tasks:
---
- name: Include OS-specific vars
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts.distribution }}_{{ ansible_facts.distribution_major_version }}.yml"
- "{{ ansible_facts.os_family }}.yml"
tags:
- "role::sshd"
- "role::sshd:install"
- "role::sshd:config"
- name: Install SSH server
ansible.builtin.import_tasks: install.yml
tags:
- "role::sshd"
- "role::sshd:install"
- name: Configure SSH server
ansible.builtin.import_tasks: config.yml
tags:
- "role::sshd"
- "role::sshd:config"
Role tagging helps later while running Ansible. When ansible-playbook
is
called with --tags
, only matching tasks will be executed.
The actual tasks are split up into individual logical units, each
within one task file. The example above e.g. splits the tasks into
installation and configuration components. Tasks inside the
install.yml
file are used to install all related packages:
---
- name: install | Install SSH-related packages
ansible.builtin.package:
name: "{{ sshd_packages }}"
state: present
The configuration files are rendered in config.yml
:
---
- name: config | Create SSH authorized_keys directory
ansible.builtin.file:
path: /etc/ssh/authorized_keys
state: directory
owner: root
group: root
mode: "0755"
seuser: system_u
serole: object_r
setype: sshd_key_t
selevel: s0
- name: config | Configure SSHd
ansible.builtin.template:
src: etc/ssh/sshd_config.j2
dest: "{{ sshd_daemon_cfg }}"
owner: root
group: root
mode: "0644"
seuser: system_u
serole: object_r
setype: etc_t
selevel: s0
validate: "{{ sshd_daemon_bin }} -t -f %s"
notify:
- Restart sshd
If necessary, you can add additional tags to individual tasks inside
the imported files. However, since this ad-hoc tag list overrides the
one defined in main.yml
, you must also provide all the tags from
main.yml
again for the single task:
Good example:
- name: install | Install SSH related packages
ansible.builtin.package:
name: "{{ sshd_packages }}"
state: present
tags:
# This tag is added only for this task
- "role::sshd:packages"
# These two tags must be provided again, as the tag list from main.yml is overwritten by this tag list.
- "role::sshd"
- "role::sshd:install"
Bad example:
- name: install | Install SSH related packages
ansible.builtin.package:
name: "{{ sshd_packages }}"
state: present
tags:
- "role::sshd:packages"
This task is no longer executed when run via --tags role::sshd
.
Variables
Variables in vars/
are used for static data, e.g. package-, service-
and filenames. Only use vars/
for data that does not change on a
host-by-host basis, for that use the defaults!
The variables stored in vars/
can be loaded dynamically. This can
be used to e.g. load OS-dependent variables. The example above uses
this to load the ssh_packages
variable dependent on the
os_family
host fact.
To achieve this, you put the variables into files named after os_family
inside the vars/
directory:
Debian.yml
RedHat.yml
If there are special variables for some operating systems, you can specify those in the files named:
Debian_11.yml
Debian_12.yml
CentOS_7.yml
CentOS_8.yml
CentOS_9.yml
Ubuntu_20.yml
Ubuntu_22.yml
…
This logic is implemented using the with_first_found
iterator in
the example above. For more information, check out the documentation
on Loops.
By our convention, each variable name start with <rolename>_
and
the name contains only lower case letters, numbers and underline
_
:
---
# ssh related packages
sshd_packages:
- openssh-client
- openssh-server
# ssh service name
sshd_service: ssh
# ssh daemon binary (absolute path)
sshd_daemon_bin: /usr/sbin/sshd
# ssh daemon configuration file
sshd_daemon_cfg: /etc/ssh/sshd_config
# ssh daemon sftp server
sshd_sftp_server: /usr/lib/openssh/sftp-server
Defaults
Every variable which is used inside a template or for tasks, and which is not defined in the vars, needs to be defined as defaults. If there is no reasonable default value, the README should make it clear that the value must be provided via host vars. Defaults can be used for example for default ports and hostnames (e.g. binding a service to localhost:80 unless overwritten via host vars).
There is only one defaults file, called main.yml
:
---
# The ports to bind sshd on
sshd_ports:
- 22
# a list of ssh host keys
sshd_host_keys:
- /etc/ssh/ssh_host_rsa_key
- /etc/ssh/ssh_host_ed25519_key
Handlers
Handlers are used to perform additional tasks required to apply changed configuration, such as restarting services. That way a service does not get restarted with every playbook run, but only when required. Another advantage of handlers is that they can be notified by multiple tasks, yet only get executed once per playbook run..
---
- name: Restart SSHd
ansible.builtin.service:
name: "{{ sshd_service }}"
state: restarted
This handler gets notified by a task called Configure SSHd
. it
will call the handler Restart SSHd
, but only if the task has
effected a change.
Using handlers should always be preferred over implementing your own conditional restart logic, unless the restart requires additional logic that can’t be covered by handlers.
Bad example:
---
- name: Render /etc/ssh/sshd_config
ansible.builtin.template: ...
register: sshd_register_sshd_config
- name: Restart SSHd
ansible.builtin.service:
name: "{{ sshd_service }}"
state: restarted
when: "{{ sshd_register_sshd_config.changed }}"
Files
If some static files have to be copied, they can be stored
in the directory files/
.
Within this directory, we rebuild the path structure of a target system. We do not store files in a flattened directory:
Good example:
sshd/
└── files/
└── etc/
├── default/
│ └── ssh
└── ssh/
└── sshd_config
Bad example:
sshd/
└── files/
├── ssh
└── sshd_config
We usually only use files/
for binary files, e.g. executables or
archives. Most text files would usually go into templates/
instead (see below); even if you don’t need to put any dynamic content
into a text file, we recommend to use a template and add an
{{ ansible_managed | comment }}
header whenever possible.
Templates
Within this directory, template files are stored with a .j2
extension as the files are treated as Jinja templates. This allows
file contents to be modified based on Ansible variables, host vars and
system facts.
Templates should have a comment with {{ ansible_managed |
comment }}
at the very beginning. This generates a comment header
inside the file, warning a potential user that changes to the file may
be overwritten. We recommend to use {{ ansible_managed | comment
}}
rather than # {{ ansible_managed }}
, as the latter does not
work with multiline ansible_managed comments. For customization of
the comment, check out the documentation of the comment filter.
If possible, validate the template before copying it into place. This will guarantee that configuration will work after restarting the corresponding service. A lot of daemon binaries come with a config test flag intended for exactly this purpose.
Good example:
---
- name: config | Configure the ssh daemon
ansible.builtin.template:
src: etc/ssh/sshd_config.j2
dest: "{{ sshd_daemon_cfg }}"
owner: root
group: root
mode: 0644
seuser: system_u
serole: object_r
setype: etc_t
selevel: s0
validate: "{{ sshd_daemon_bin }} -t -f %s"
notify:
- "Restart SSHd"
Within the template/
directory, we rebuild the path structure of a target system. We
do not store templates in a flattened directory.
Good example:
sshd/
└── templates/
└── etc/
├── default/
│ └── ssh.j2
└── ssh/
└── sshd_config.j2
Bad example:
sshd/
└── templates/
├── ssh.j2
└── sshd_config.j2
Meta
The file meta/main.yml
contains metadata about a role. For
standalone roles, this file is required in order to be submitted to
Ansible Galaxy. For roles in a collection, this file is optional, but
recommmended.
---
galaxy_info:
author: 'Adfinis AG'
description: 'Install and manage sshd'
company: 'Adfinis AG'
license: GPL-3.0-only
min_ansible_version: 2.10
platforms:
- name: Debian
versions:
- buster
- bullseye
- bookworm
- name: Ubuntu
versions:
- jammy
- lunar
- mantic
- name: CentOS
versions:
- 7
- 8
- 9
galaxy_tags:
- ssh
- sshd
# The roles listed here are automatically applied before applying this role.
dependencies:
- role: adfinis.linux
Collections
Collections are the new format for packaging roles, plugins, playbooks and other Ansible artifacts.
For more in-detail information, please refer to the upstream documentation: Developing collections.
A new collection can be created using the command ansible-galaxy
collection init <namespace>.<collection>
. The collection will be
created in the directory ./<namespace>/<collection/
.
We also provide a Github template for Ansible collection repositories, which comes with a CI pipeline for ansible-lint and automated release to Ansible Galaxy: adfinis/ansible-collection-template.
Artifacts in a collection should always be referred to by their FQCN
(fully-qualified collection name) consisting of
<namespace>.<collection>.<artifact>
. For example, the role
sshd
in the collection adfinis.linux
is referred to as
adfinis.linux.sshd
. The same applies to other artifacts such as
plugins or playbooks as well.
Collection Directory Layout
.
├── docs/
├── galaxy.yml
├── meta/
│ └── runtime.yml
├── plugins/
│ ├── callback/
│ ├── inventory/
│ └── modules/
│ └── example.py
├── README.md
├── roles/
│ ├── sshd/
│ └── pki/
├── playbooks/
│ ├── playbook.yml
│ ├── templates/
│ └── tasks/
└── tests/
galaxy.yml
The galaxy.yml
file at the root of your collection contains the
metadata required in order to publish your collection to Ansible
Galaxy:
---
namespace: adfinis
name: linux
version: "1.0.0"
readme: README.md
authors:
- Adfinis AG <support@adfinis.com>
repository: http
description: Collection of roles for basic configuration of a Linux server
license: GPL-3.0-only
tags:
- linux
dependencies:
community.general: "7.5.0"
community.crypto: "2.15.1"
repository: https://github.com/adfinis/linux
documentation: https://adfinis.github.io/...
homepage: https://adfinis.com
issues: https://github.com/adfinis/linux/issues
build_ignore: []
meta/runtime.yml
Usually this file only contains one entry: Which Ansible version is required to use this collection:
---
requires_ansible: ">=2.10.0"