Optimizing IT Automation: Best Practices for Ansible
Ansible is an exceptionally flexible and user-friendly automation tool. That’s ideal, because you can quickly get started using it to automate your tasks. At the same time, this also means there are many different ways to set up Ansible. That’s why tips and best practices help ensure everything runs smoothly in the long run and saves you time. Without further ado, below we share a few key points we’ve learned over the past ten years working with Ansible, Tower, and AWX.
Bronchial Management
- Use a source code management system, preferably Git.
- Never commit directly to the master or main branch. Seriously, 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 pin a specific version and trust that their code will continue to work.
- One Git repository 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 repositories, it’s a good idea to move your `environments` directory to a separate repository so you don’t have to maintain multiple sets of 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 features such as anchors and aliases.
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 use 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 take advantage 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 long list of packages, you can also use `package_facts` and then apply a list difference to determine the final list of packages to install.
Always apply the principle of least privilege. 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 you use to `requirements.yml`. AWX automatically downloads these collections before the playbook is executed.
Configuration file ansible.cfg
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
Since the sample structure places all environments within a single project, it’s a good idea to set the default inventory to point to the environment that causes the least damage if something goes wrong. This way, when running playbooks from the command line, you’ll need to 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. During the initial installation of a host, create a separate temporary directory with permissions restricted exclusively to the designated Ansible remote user. This prevents a local attacker (on the target system) from gaining access to temporary data or scripts that Ansible pushes to the host, as long as that 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 more than ten years 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 toward automation as efficient and effective as possible.
To learn how SUE manages and migrates Linux environments, visit the Linux page. The " Choosing a Linux Distribution" guide will help you choose your next distribution.