/ansible-style-guide

Style guide for writing Ansible

GNU General Public License v3.0GPL-3.0

Ansible Style Guide

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.

Components

Roles

Name

Role names must follow kebab-case and should be in singular form.

Location

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).

Implementation

Interfaces

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).

Handlers

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.

Re-use

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.

Playbooks

Name

Playbook names must follow kabab-case and should be in plural form.

Implementation

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.

Groups

Name

There are three distinct types of Ansible group:

  1. Groups containing variables that are used in a single playbook (e.g. configurations in the irobots group are used exclusively with the irobots.yml playbook). These groups should take the same name as the playbook, which should be in plural form.
  2. 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.
  3. 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.

Location

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

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.

Variables

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 form hail_*, such as hail_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 *_* 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 the hail_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