Skip to content

Ansible Style Guidelines

This guide lays out some must and should rules for how to write Ansible playbooks, rules etc. It covers both Ansible style and convention as well as common patterns such as software installation, systemd service configuration etc. Rules that are already covered by ansible-lint are not included.

YAML Basics

  • All Ansible YAML files must have the extension .yml, not .yaml.
  • If required, a comment explaining what a YAML file is for (full sentence description, empty line, longer description) can be places above the document start marker like this:

    # Install and configure foobar # # This is a rule that configures and installs the popular foobar program.


  • Only quote strings when YAML syntax requires it.

  • When quotes are required, use double quotes unless single quotes are required.

Ansible Basics

  • Follow Ansible best practices.
  • Don't prefix task names with some role identifier or similar, Ansible automatically prepends this.
  • Use blocks to group tasks that you want to execute/skip in unison based on some condition.
  • Prefer copy/template over content/lineinfile/blockinfile.
  • Prefer synchronize over copy for moving larger files (if rsync is present both locally and on target).
  • ALWAYS use no_log when handling sensitive output in a task.
  • Prefer not to use debug: in committed playbooks.

Playbooks

Currently every playbook completely sets up a certain machine type. This may change in the future.

Add these to a playbook in the following order (if used):

  • hosts
  • become
  • vars
  • vars_files
  • pre_tasks
  • roles
  • tasks

Prefer handling things in reusable roles instead of adding playbook-specific tasks. Most playbooks should likely include the basic_tools which installs utilities for live-debugging/maintenance. For consistencty always prefix with role: even when not specifying any variables, e.g.:

roles:
  - role: docker
  - role: cuda
  - role: consul
    vars:
      consul_node_name: # ...

Roles

Structure

Every role should most likely (perhaps with special exceptions such as roles that don't contain tasks but only handlers) contain at minimum the following files:

foobar_client
|- README.md
|- meta
|  |- main.yml
|  `- argument_specs.yml
|- defaults
|  `- main.yml
`- tasks
   `- main.yml

README

README.md should contain a title (the role name in title case), a brief description of what the role does and a usage example, e.g.:

# Foobar Client

This installs a client for the Foobar service. The client can be used to invoke
various foobar functionalities.

Dependencies, options etc. should not be listed here as they are documented elsewhere (see next sections).

meta/main.yml

meta/main.yml should contain a short galaxy block with the following fields, all of which except description (the role name in sentence case) should exactly match this example unless there is a very good reason to deviate from it:

---
galaxy_info:
  author: Lyceum GmbH
  description: Foobar client
  company: Lyceum GmbH
  license: proprietary
  min_ansible_version: 2.18
  platforms:
    - name: Ubuntu
      versions:
        - noble

After this there may follow a dependencies block that lists local and third-party dependencies. One dependency that you'll probably want to make available in most roles is systemd_restarter which defines common handlers for restarting systemd services. For example:

dependencies:
  - role: systemd_restarter
    vars:
      systemd_service: foobar-client

Which you can then use to notify e.g.:

notify: "systemd_restart : Restart foobar-client systemd service"

This helps keep handler naming and usage patterns consistent across roles.

For other dependencies it usually makes more sense to directly add them in a task via ansible.builtin.import_role to make its usage more explicit.

meta/argument_specs.yml and defaults/main.yml

meta/argument_specs.yml must contain types and descriptions of all options that the role expects. No variables a role consumed must be explicitly declared in argument_specs.yml. We try not to go overboard with the number of available options so certain potentially configurable settings can be "hardcoded" within the role, commonly:

  • Installation/config directories
  • Systemd service names

On the other hand these should be options for future-proofing:

  • Binary versions
  • Users/groups under which services run
  • Data/log directories
  • Systemd service states (but defaulting to enabled: true, state: started)

By convention all options are prefixed by the role name and sorted alphabetically within argument_specs.yml, Descriptions are mandatory.

---
argument_specs:
  main:
    options:
      foobar_client_dockerized:
        type: bool
        default: false
        description: Whether to run Foobar client dockerized
      foobar_client_unsafe:
        type: bool
        default: false
        decription: Whether to enable unsafe (HTTP) communication for Foobar client
      foobar_client_version:
        type: str
        default: 1.0.0
        description: Foobar client version

Any default must then also be present in defaults/main.yml, again sorted alphabetically. Additional comments are not required here, all information should be centralized in argument_specs.yml. For example:

---
foobar_client_dockerized: false
foobar_client_unsafe: false
foobar_client_version: 1.0.0

Great care must be taken to keep defaults here and in argument_specs.yml in sync, Ansible unfortunately cannot derive one from the other by itself.

Tasks

If you have a long list of tasks for a given role that can be logically split up into several larger steps, consider splitting them up into several task files, e.g.:

tasks
|- main.yml
|- install_client.yml
`- configure_client.yml

With main.yml then containing:

---
- name: Install Client
  import_tasks: install_client.yml

- name: Configure Client
  import_tasks: configure_client.yml

Handlers

Some roles might require additional handlers not provided by systemd_restarter. These should all be declared in handlers and not inside of task files. When referring to these handlers always prefix them with the role name to avoid any ambiguity, e.g. foobar_client : Reload client config.

Files and Templates

If your role needs to create files (configurations etc.), do not manage them inline in a task with content: but instead via the files and templates directories. The former contains files that don't need to be processed by Jinja and the latter all other files. Make sure your template files thus have a .j2 extension, e.g. foobar-client.service.j2