Optimizing IT Automation: Best Practices for Ansible
Ansible is a highly flexible and user-friendly automation tool. This is ideal, because you can quickly get started automating your tasks. At the same time, this also means that there are many different ways to set up Ansible. That is precisely why tips and best practices are essential to keep everything running smoothly in the long term and save yourself time and effort. Without further ado, below we share a number of points we have learned over the past ten years of working with Ansible, Tower, and AWX.
Source code management
- Use a source code management system, preferably GIT.
- NEVER commit directly to MASTER/PRIMARY. Really, don't. The master branch should always contain working, tested, and functionally correct code (as far as you know).
- All development is done on a separate branch. Test and review it, and only merge it into the master branch once it has been proven that everything works correctly.
- Use version tags for your roles. Tag your roles with every change. This is not only useful for yourself, but especially for others who use your roles. This allows them to lock in a specific version and trust that their code will continue to work.
- One GIT repo per role. This makes it easy for others to include your roles in their playbooks.
- Use one or more separate repositories for your playbooks. If you use multiple repos, it is wise to split your environments directory into a separate repo, so you don't have to maintain multiple sets with the same variables.
Directory structure
Just use the standard directory structure for your roles. You can easily create a new role structure with ansible-galaxy init.
For your playbooks and inventories, you can use the following structure, for example:
/
├── ansible/
│ ├── ansible.cfg
│ ├── environments/
│ │ ├── 00-superglobal.yml
│ │ ├── dev/
│ │ │ ├── hosts
│ │ │ └── group_vars/
│ │ │ ├── all/
│ │ │ │ └── 00-superglobal.yml
│ │ │ │ └── 10-allvars.yml
│ │ │ ├── group_1
│ │ │ └── group_2
│ │ ├── prod/
│ │ │ └── group_vars/
│ │ │ ├── all/
│ │ │ │ └── 00-superglobal.yml
│ │ │ │ └── 10-allvars.yml
│ │ │ ├── group_1
│ │ │ └── group_2
│ │ └── sandbox/
│ │ └── group_vars/
│ │ ├── all/
│ │ │ └── 00-superglobal.yml
│ │ │ └── 10-allvars.yml
│ │ ├── group_1
│ │ └── group_2
│ └── playbooks/
│ ├── playbook.yml
│ └── roles/
│ └── requirements.yml
└── test/
Copy code
In this tree structure, you can see four 00-superglobal.yml files. The files in the all directories are symlinks to the file in the environments directory. This gives you a way to define variables across all environments without having to repeat them each time.
group_1 and group_2 can be either files (which must end with .yml) or directories containing YAML files.
I like to give variable and inventory files a numeric prefix, because Ansible processes the files in group_vars and inventories in lexicographic order. This allows you to control the reading order of variables, which can be useful when using YAML mechanisms such as references and pointers.
Playbook structure
If you have many tasks in a single playbook, it is wise to consider converting these tasks to a role.
Do not set variables within a playbook (if you can avoid it). I will discuss this in more detail later in this document.
If you want to include other playbooks for execution within the playbook you are working on, it is recommended that you convert that other playbook to a role.
Structure your playbook logically. Even if you don't work according to the pre-tasks, roles, and post-tasks format, it's wise to structure your playbook in a similar way.
If you need to install multiple packages on a yum-based system, use the yum module instead of package and make use of the list functionality within the name section. This is much faster than using package with a list of packages. If you have a very large list of packages, you can also use packages_fact and then apply a list difference to determine the final list of packages to install.
Always use the lowest possible permission level. Do not use a general become: yes at the top of your playbook. Yes, it is easy and convenient, but you also run the risk of unintentional changes to privileged files and settings.
Role inclusion
Use roles/requirements.yml to include roles in your playbook. This prevents code duplication. Always pin the version of the role you want to use. This is what roles/requirements.yml looks like.
If you want to use playbooks with different versions of the same role, create a separate playbook directory for each playbook, with its own roles/requirements.yml file.
Collections inclusion
Use collections/requirements.yml to include collections in your playbook. Not all collections and modules are available by default in AWX (for example, maven_artifact from community_general). You must explicitly add the collections used to requirements.yml. AWX automatically downloads these collections before executing the playbook.
Ansible.cfg configuration file
Ansible supports multiple ways to configure its behavior, including an ini file named ansible.cfg. Place this file in the directory above the directories containing playbooks, roles, and environments.
[defaults]
inventory = environments/sandbox
host_key_checking = False
remote_tmp = /var/tmp/.ansible/tmp
remote_user = ansible
roles_path = roles
collections_path = collections
Copy code
Host key checking is disabled by default in AWX. In Ansible Tower, this check can be enabled because Tower is host-based rather than container-based. To do this, you must populate the known_hosts file for the Tower application user before you can use a host in Tower.
Explanation of the settings
inventory = environments/sandbox
Because you have all environments in one project in the example structure, it is wise to have the default inventory point to the environment that causes the least damage if something goes wrong. This means that when executing playbooks via the command line, you must explicitly specify a more critical environment.
host_key_checking = False
This disables SSH host key checking. In AWX, this is the default setting because playbooks are executed in a separate execution container that can be deleted at any time. Since the SSH user can differ per playbook, there is no easy way to inject a known_hosts file into AWX.
Please note: host key checking is only a good security measure if the host keys are known from the outset. When using Ansible from the command line on a persistent host, I strongly recommend leaving host_key_checking set to true (i.e., omitting the setting), which is also the default setting in Ansible.
remote_tmp = /tmp/.ansible/tmp
This can serve as an additional security measure. When initially installing a host, create a separate temporary directory with exclusive rights for the intended Ansible remote user. This prevents a local attacker (on the target system) from accessing temporary data or scripts that Ansible pushes to the host, as long as this attacker does not have elevated privileges.
remote_user = ansible
This is the default remote user. In AWX, this setting is overridden by the machine credential set in the playbook template.
Variable definitions and their location
Naming variables
Use underscores for longer variable names. Please do not use CaMeLCaSiNg. Your variable name can be a little longer; this improves readability and you really don't need to save characters.
According to the YAML 1.1 specification, simple keys can even be up to 1024 characters long.
Use clear variable names. In simple loops, you can get away with using a single letter as an iterator, but readability improves greatly when you use descriptive names.
If you choose a different naming convention, do so consistently and use the same approach everywhere.
Lists
I like to use an extra indentation level when defining lists. This increases readability. In the example below, the difference seems small, but with complex dictionaries, the advantage quickly becomes clear.
listdefinition:
- item1
- item2
- item3
nextdefinition:
- item_a
- item_b
- item_c
something_else
Copy code
YAML anchors
YAML has a useful feature for reusing configuration via so-called anchors. With the & syntax, you can define a configuration and reuse it later:
- name: this is a first task
action1: &config
username: test
password: "{{ password }}"
state: present
config1: true
- name: this is a second task
action2:
<<: *config
state: absent
config2: false
Copy code
This shorthand prevents duplication of frequently used options such as usernames, passwords, URLs, namespaces, and so on.
Need help with Ansible?
With over a decade of experience using Ansible, Tower, and AWX to streamline and secure IT operations, our team has perfected automation down to the last detail. Our Ansible best practices help make your journey to automation as efficient and effective as possible.