Abstract
This document simply refers to the good practices document from the field of Ansible practitioners at Red Hat, consultants, developers, and others. and also tips and tricks from Ansible community. Please visit the related pages to get more deep understanding if needed.
The following rules are a good recommendation to start with. Optimize for Readability should be the main indicator and if done properly, it can be the documentation of your workflow automation.
A clean separation of code and data (aka inventory) needs to be fostered
from the beginning. In general one basis version control system
repository for all the project code and documentation, including all
roles in a sub-directory and another version control system repository
called for the inventory and variables. The root directory in each
repository contains a README.adoc
file which explains the repository
and the sub-directories structure and/or points to more detailed
documentation.
The project contains following repositories:
-
Code repository for roles, collections, playbooks is named
rhis-code
-
Inventory and variable repository is placed named
rhis-inventory
High level hierarchy for playbooks
The following automation structure model is used in the project.
Landscape
Top level playbooks are separated by role. A landscape is anything we
want to deploy at once which includes many import of other playbooks and
can be visualized like a workflow approach in Ansible Automation
Controller. The named should be include a prefix named landscape_
.
Use Cases (Types)
Use cases are high level collections of tasks, grouped around a specific
type of usage - base operating system configuration, a particular server
deployment etc. Use cases are mapped as epics. These epics
contain user stories, tasks and bugs for specific features. These kind
of playbooks should start with a prefix named type_
which can be
easily used on Ansible Automation Controller. Each type is then made of
multiple functions, represented by roles, so that the same function used
by the same type can be re-used, written only once.
Features (Functions)
Depending on the complexity and distinct requirements, a use case might
contain one or more features. Features are generally mapped to user
stories in a particular epic. It is preferred to create a simple
playbook named with a prefix named function_
for a particular role,
mostly to be used as a ad-hoc script.
Helper playbooks (Toolboxes)
Non use case playbooks and scripts, generally the required for cleanup,
testing etc., are allowed as long as they are explicitly named starting
with toolbox_
prefix. They are self contained and do not introduce
additional dependencies or blockers for the use case features.
Roles
-
All new roles should be placed under the standard folder
roles
. -
New roles should be initiated in line, with the skeleton directory, which has standard boilerplate code for a Galaxy-compatible Ansible role and some enforcement around these standards.
-
All defaults and all arguments to a role should have a name that begins with the role name to help avoid collision with other names.
-
Design roles focused on the functionality provided, not the software implementation. Break down your playbooks into reusable roles. Each role should have a clear and specific purpose.
-
All roles have a
meta/main.yml
file with the fields values documented in alignment with guidelines_automation_header.yml. In normal cases, only thedescription
should need specific values, and potentially themin_ansible_version
. -
No empty directories, or directories with useless files.
-
All roles have a
README.md
file with at least an explanation of the purpose of the role; variables don’t need to be repeated. The documentation is essential for the success of the content. Place theREADME.md
file in the root directory of the role. -
defaults/main.yml
contains all variables able to influence the role, together with comments on usage. The variables can be commented out if their default is to be undefined but they have to be listed. -
All variables defined within a role have a prefix corresponding to the role name to create a kind of namespace (see also variable names in link:Inventory and Variables[Inventory and Variables]).
-
Use handlers for tasks that should only run when notified. Handlers should have clear names and be defined in the handlers section of a role.
-
Follow a consistent naming convention for roles. Use lowercase letters and separate words with underscores.
-
Documentation: Include a README.md file in each role to document its purpose, variables, and usage. Document playbook dependencies and any prerequisites.
-
Internal variables (those that are not expected to be set by users) are to be prefixed by two underscores like `__foo_variable`.
-
Prefix all tags within a role with the role name or, alternatively, a "unique enough" but descriptive prefix.
-
Do not use dashes in role names. This will cause issues with collections.
-
The role should work in check mode, meaning that first of all, they should not fail check mode, and they should also not report changes when there are no changes to be done.
-
Reporting changes properly is related to the other requirement named
idempotency
. Roles should not perform changes when applied a second time to the same system with the same parameters, and it should not report that changes have been done if they have not been done. -
Add
{{ ansible_managed | comment }}
at the top of the template file file to indicate that the file is managed by Ansible roles, while making sure that multi-line values are properly commented. -
It is a common practice to have tasks/main.yml file including other tasks files, which we’ll call sub-tasks files. Make sure that the tasks' names in these sub-tasks files are prefixed with a shortcut reminding of the sub-tasks file’s name.
Playbooks
-
YAML documents (including Ansible playbooks) MUST begin a document with
---
followed by one empty line. -
Files SHOULD use long-form YAML, not short-form YAML.
person: {name: ansible test, address: {street: somewhere, city: Anycity, state: HE, zip: 12345}}
use long-format YAML
person:
name: ansible test
address:
street: somewhere
city: Anycity
state: HE
zip: 12345
-
Every task, play or block MUST have a name.
-
Every task, play or block MUST have 1 or more appropriate tags, annotated as an array instead of a single string. Also document tags and their purpose(s).
-
Strings which are not builtin parameter triggers like
present
are enclosed in quotes to make them stand out to the reader. -
Use Ansible modules in tasks instead of commands if available.
-
Use fully qualified collection names (FQCN) to avoid ambiguity in which collection to search for the correct module or plugin for each task.
-
For builtin modules and plugins, use the
ansible.builtin
collection name as a prefix, for exampleansible.builtin.copy
. -
Tasks using the
command
orshell
module (or, more generally, any module that doesn’t report if it changed anything) have achanged_when
field to indicate if they changed something. If a tasks merely collects information appendchanged_when: false
to the task. -
Use only the
true/false
boolean style, Do not useyes/no
orTrue/False
-
No privilege escalation unless absolutely necessary. Using
become
at the playbook context is strongly discouraged. All roles should includebecome
for tasks that need privileged execution. To keep the tasks design simple,block
orinclude_tasks
can also be used depending on the design. -
A playbook can contain pre_tasks, roles, tasks and post_tasks sections. Avoid using both roles and tasks sections, the latter possibly containing import_role or include_role tasks.
-
It is a good practice to enable debug messages only if a higher level of verbosity is specified while launching the playbook.
- name: this message will only appear when verbosity is 2 or more debug: msg: "Some more debug information if needed" verbosity: 2
-
Do not use special characters other than underscore in variable names, even if YAML/JSON allow them.
-
Use a single space separating the template markers from the variable name inside all Jinja2 template points. For instance, always write it as {{ variable_name_here }}
-
Use double quotes for YAML strings with the exception of Jinja2 strings which will use single quotes.
-
Always mention the state. For many modules, the
state
parameter is optional. Different modules have different default settings for state, and some modules support several state settings. Explicitly settingstate: present
orstate: absent
makes playbooks and roles clearer. -
Use the serial keyword to control how many machines you update at once in the batch.
-
Name your playbooks descriptively, reflecting their purpose or the infrastructure component they manage.
-
Define variables in a separate vars section or in separate variable files. Use meaningful variable names and provide comments for complex or non-obvious cases. Please refer to the section link:Inventory and Variables[Inventory and Variables].
-
Implement proper error handling in your playbooks. Avoid to use the
ignore_errors
andfailed_when
attributes judiciously.
Collections
- All required collections should be defined in the
collections
folder. The best practice is to list all the required collections in therequirements.yml
file, and let AAP controller install them as needed.
Inventory and Variables
-
Define your inventory as structured directory instead of single file.
-
Directory Structure include
group_vars
andhost_vars
directories. -
group_vars
folder is the main location for variable and parameter files for a given group. The parameters defined here are valid for all hosts that belong to the relevant group. -
host_vars
folder is the main location for variable and parameter files for a given host. -
All hosts are defined by their FQDNs.
-
Use dynamic inventory with cloud approach and build smart inventories if needed.
-
Variables should written in the separate files that is simplifying to use by grouping.
-
Keep vaulted variables safely visible. Encrypt sensitive or secret variables with Ansible Vault. However, encrypting the variable names as well as the variable values makes it hard to find the source of the values. To circumvent this, you can encrypt the variables individually using
ansible-vault encrypt_string
. -
Do not populate the same variable in more than one hierarchy.
-
Don’t use
extra vars
to define your desired state. Make sure your inventory completely describes how your environment is supposed to look like. Use extra vars only for troubleshooting, debugging or validation purposes. -
Avoid playbook and play variables, as well as include_vars. Opt for inventory variables instead.
-
Use parent/child group relationships. It is always good to use parent/child relationships among groups. Parent groups are also known as nested groups or groups of groups.
-
Variables should be prefixed with the related application, Ansible Role, etc, to bring context and avoid with the name of the role.
YAML / Jinja2 Syntax
-
Indentation should be 2 spaces. Further, arrays (i.e., lines which begin with -) should be indented 2 spaces from its parent as well.
-
use two spaces indentation width in YAML files; Do not use tabs.
-
Lines MUST NOT have trailing whitespace. Remove all trailing whitespaces before you commit a file.
-
A file MUST end with a single empty line feed.
-
Files MUST use only UTF-8 without BOM as its character encoding.
-
Split long expressions into multiple lines.
-
Code that is no longer needed is removed and not commented out.
-
Avoid comments in playbooks when possible. Instead, ensure that the task name value is descriptive enough to tell what a task does. Variables are commented in the defaults and vars directories and, therefore, do not need explanation in the playbooks themselves. If needed, comments MUST have a single space after
\#
for better readability. -
There SHOULD always be one line of whitespace above the start of a comment block.
-
When naming files, use the
.yml
extension and not.yaml
for YAML files,.j2
as the file extension for Jinja files. -
Use lowercase a-z for naming the files. Use underscore _ as a separator, do NOT use dashes.
-
Do NOT use whitespace as a separator.
General Ansible Guidelines
-
Ensure that all tasks are idempotent.
-
If there is ever a discrepancy between English-speaking dialects, use U.S. English as a baseline for both spelling and word choice.
-
Use the
| bool
filter when using bare variables (expressions consisting of just one variable reference without any operator) in when. -
Use bracket notation instead of dot notation for value retrieval (e.g.
item['key']
vs.item.key
) -
Do not use
meta: end_play
as it aborts the whole play instead of a given host. -
Avoid the use of
lineinfile
wherever that might be feasible. -
Enable Callback plugins. Callback plugins enable adding new behaviors to Ansible when responding to events. By default, callback plugins control most of the output you see when running the command line programs, but can also be used to add additional output, integrate with other tools and marshal the events to a storage backend.
-
Use tools like
ansible-lint
to check your code for common issues and adherence to best practices. Maintain consistent code formatting. -
Implement testing for your Ansible code, either manually or through automated testing frameworks.