When creating a large Ansible configuration with a team, it is important to align to a set of conventions in order to achieve a setup that is easy to maintain and extend. This guide is intended to complement the best practice guidelines supplied by Ansible, with additional rules to help when a configuration becomes big, complex and is managed by multiple developers.
Role names must follow kebab-case and should be in singular form.
Roles must be placed inside the roles
directory. All variables that can be used to configure the role must be
mentioned in the defaults/main.yml
file. If it is not appropriate for a default value to be set, the variable is to be
listed at the top of the file in a comments section and should be in the form:
# Variables that have no defaults and must be set:
# $variable_name:
# [($python_style_data_type)] $description
For example:
# Variables that have no defaults and must be set:
# hail_jupyter_token:
# (str) an authentication token used to login to Jupyter notebook
A task should be added to the start of the role to check that all required variables have been set. The assert
module
could be used for this purpose (remember to use no_log
or msg
appropriately to prevent secrets from being
accidentally spilled).
There must be a clean interface through which roles are configured; role variables are to be set in plays, such that
all values used to configure the role are binded to role parameters via the associated vars
section, e.g. in the
hailers.yml
playbook:
- hosts: hailers
roles:
- role: hail
vars:
hail_version: "{{ hail_GROUP_version }}"
hail_cluster_name: "{{ hail_SPECIFIC_GROUP_name }}"
A role must not "reach out" and use variables that happen to be in scope, as this can both couple roles to specific
setups, and it can to make it difficult to know where a value used in a role has been defined. Roles must therefore only
use variables mentioned in default
or vars
files (with the exception of the use of built-ins).
By default, handlers are executed at the end of a play. This is problematic if a failure occurs, as the triggering change may not occur on the re-run, and subsequently the handler may never fire. For this reason, use of Ansible handlers should be kept to a minimum.
Note: the same problem can occur with registering variables and executing subsequent actions based on whether a change occurred.
It is good practice to design roles such that they could be reused by others. Ideally, such roles would be developed and tested in separate repositories, then imported using Ansible Galaxy or by use of git-subrepo (or similar). However, this will not be appropriate for some, very project specific roles.
Playbook names must follow kabab-case and should be in plural form.
It is often useful for playbooks to first assert hosts are members of the correct groups - this will lead to fewer variable undefined related surprises later.
There are three distinct types of Ansible group:
- Groups containing variables that are used in a single playbook (e.g. configurations in the
irobots
group are used exclusively with theirobots.yml
playbook). These groups should take the same name as the playbook, which should be in plural form. - Groups of general configuration values, which are used in many playbooks (e.g. configurations for your GitLab
account, such as
gitlab_url
,gitlab_token
). Such groups should have a relevant name and be in singular form. - Groups containing variables that are specific for a set of hosts, which shall be referred to as "specific groups".
For example, the group
hail-cluster-bob
could define variables used by Bob's Hail cluster. These specific groups should have a relevant prefix, followed by the name of the cluster. The name should be in singular form.
All group names must be kebab-case.
All files containing group variables must to be located in sub-directories in group_vars
. Plaintext variables must
be defined in a file called vars.yml
and secret variables are to be placed in an
Ansible Vault named vault
. To aid searchability, all
variables defined in Ansible vaults must be referenced in the header of the associated vars.yml
file, in the format:
# Value contains:
# example_GROUP_variable
Tasks should always have a descriptive name. The first word should not be capitalised by default but capital letters may be used for proper nouns and acronyms. The name does not need to be prefixed with the role name and should not be constructed programmatically.
Ansible setups can defines hundreds of variables, which can be changed to customise the setup. When using Ansible at scale, it is important to follow naming conventions to be able to easily work with so many variables.
All Ansible variables must be namespaced to enable developers to easily find where they have been defined and to avoid naming collisions. By looking at the name alone, it should be possible to know what file(s) the variable has been defined in, without false positives.
Variables must be:
- Descriptive and not use obscure acronyms or abbreviations.
- Snake case (i.e.
my_example_variable
). - Prefixed with the role/group/play to which the variable belongs. For example, variables for the role
hail
are to be in the formhail_*
, such ashail_version
,hail_ssl_key_file
). - Started with
_
if they are meant to be private to the defining file (private variables must still follow all other rules). - Indicative as to where they have been defined, according to the rules in the table below:
Type Name Description Defined In Role *_ROLE_*
The prefix matches the name of the role, followed by an underscore, then a suffix that describes the variable's purpose. roles/$0/defaults/main.yml
,roles/$0/vars/main.yml
Group *_GROUP_*
The prefix matches the snake-case version of the group name, followed by
_GROUP_
, then a suffix that describes the variable's purpose.Often, the suffix will match that of the role variable to which the group variable corresponds. For example, the value of the group variable
hailers_GROUP_version
may be binded to thehail_version
role parameter.group_vars/$0/vars.yml
,group_vars/$0/vault
Specific Group *_SPECIFIC_GROUP_*
The prefix matches the snake-case version of the name of the specific group, followed by
_SPECIFIC_GROUP_
, then a suffix that describes the variable's purpose.For example, the specific group
consul-cluster-hgi
may define the variable:consul_cluster_SPECIFIC_GROUP_id: hgi
.group_vars/$0s/vars.yml
,group_vars/$0s/vault
Playbook *_PLAYBOOK_*
Defined in the vars
section of a play, with a name prefix matching the snake-case version of the playbook name (in singular form), followed by_PLAYBOOK_
, then a suffix that describes the variable's purpose. In general, playbook variables should only be defined if they are to be used multiple times.$0s.yml
Host *_HOST_*
The prefix should relate to the general purpose of the variable, followed by _HOST_
, then a suffix that describes the variable's purpose.host_vars/$0s.yml
Playbook Fact *_PLAYBOOK_FACT_*
A playbook fact is a variable defined in a playbook with
set_fact
, intended for use outside of the play where it was set. Use of such variables should generally be kept to a minimum.The prefix should match the snake-case version of the name of the play, followed by
_PLAYBOOK_FACT_
, then a suffix that describes the variable's purpose.$0s.yml
Role Fact *_ROLE_FACT_*
A role fact is a variable defined in a role with
set_fact
, intended for use outside of that role. Use of such variables should generally be kept to a minimum.The prefix should match the name of the role, followed by
_ROLE_FACT_
, then a suffix that describes the variable's purpose.roles/$0/tasks/*.yml
Role Register *_ROLE_REGISTER_*
A role register is a variable defined implicitly within a role by adding a
register
property to a task definition. These should _only_ be referenced from within the role itself (otherwise you should promote them to a fact as above), but nonetheless it is useful to namespace them to avoid collisions with other variables, including possibly a subsequent role fact with the same suffix.The prefix should match the name of the role, followed by
_ROLE_REGISTER_
, then a suffix that describes the variable's purpose.roles/$0/tasks/*.yml