Ansible Best Practices
Ansible is a tool that automates configuration management through code written in YAML. It is extremely popular, partly thanks to the fact that it works without clients and daemons: all it needs is Python and an SSH connection!
More efficient, faster, automated: Taking your IT infrastructure to the next level
Discover everything to do with infrastructure automation in our free live and on-demand webinars - from best practices to specific tool insights.
👉 Find out more now and secure valuable know-how!
Since the YAML syntax is quite user-friendly, it is fairly easy to get started with Ansible. However, without a thorough introduction to the formats and structures of Ansible, the initial result is usually “bad” code: it might work, but it is prone to yield errors in the future, be inefficient or simply unreadable.
In this article, we want to present a few of the Ansible best practices that we deem especially important. This is a taste of what we discuss during our Ansible trainings at ATIX. If you are interested in learning Ansible from the ground up with a hands-on approach, do not hesitate to contact us for more information about our trainings.
YAML
-
Use
true
instead ofyes
andfalse
instead ofno
-
Insert two blank spaces per indentation level (not four and NEVER tabs!)
-
Avoid inline lists and dictionaries
-
Break lines that are longer than 80 characters:
1 a_string: > # transforms linebreaks to spaces
2 R0lGO2313DdhDQAIAIAAAAAAANn
3 Z2SwAAAAAsdasfDQAIAAACF4SDGQ
4 ar3xxbJ9p0qa7R3fasdAS0YxwzaFME
5 1IAADs=
6 7 # this string doesn't have spaces
8 a_folded_string_without: "R0lGO2313DdhDQAIAIAAAAAAANn
9 Z2SwAAAAAsdasfDQAIAAACF4SDGQ
10 ar3xxbJ9p0qa7R3fasdAS0YxwzaFME
11 1IAADs="
Playbook
-
Task names:
-
Always give a name to each task
-
Start the name with a capital letter
-
Ensure the name describes the task appropriately; for example “Install Apache” instead of “Install stuff”
-
-
Use dictionary style in playbooks, for example:
1 # avoid this
2 - name: Install Apache packages
3 yum: name="{{ packages }}" state=present
4 vars:
5 packages:
6 - httpd
7 - httpd-devel
8 9 # use this instead
10 - name: Install Apache packages
11 yum:
12 name: "{{ packages }}"
13 state: present
14 vars:
15 packages:
16 - httpd
17 - httpd-devel
-
Use the fully qualified collection name (FQCN), e.g.,
ansible.builtin.debug
instead ofdebug
-
Apply privilege escalation at task level, not at play level
-
Try to avoid the modules
ansible.builtin.command
,ansible.builtin.shell
andansible.windows.win_shell
: these modules are not idempotent! If you have no other option, try to work withchanged_when
,failed_when
and (if applicable)creates
andremoves
to make your tasl as idempotent as possible
Vaults
-
Encrypt all sensitive variables/files using ansible-vault
-
Store the vault password securely, for example as a Jenkins secret or by using an external secret manager, such as HashiCorp’s vault
-
If you encrypt a whole file, you may forget the names of your variables => add layer of indirection for readability, for example
1 ---
2 # vault.yaml
3 vaulted_password: this_will_be_encrypted
1 ---
2 # vars.yaml
3 password: "{{ vaulted_password }}"
Reusing Ansible Code
-
Separate your tasks into files and then import/include them in your playbook
-
As there are some important differences between
import_tasks
andinclude_tasks
(see this table), try to useimport_tasks
when possible -
In general, go for
import_*
, especially because-
--list-tasks
also shows the imported tasks (and--start-at-task
works as intended) -
--list-tags
also shows the tags of the imported tasks -
notify
of imported tasks works (careful: you should notify one of the imported tasks, not the import_tasks task itself!) -
the options of the
import_*
are passed to ALL child tasks
-
-
You should use
include_*
when-
you want to loop over tasks
-
the name of the file to be included depends on a variable, for example
{{ ansible_os_family }}.yaml
-
you want to apply an option ONLY to the
include_*
task
-
Roles
-
Collect Ansible content meant for reuse in future projects into roles
-
Use one of the two options to define variables within a role—
defaults
andvars
:-
vars
refers to variables that are not supposed to be changed by the user -
defaults
refers to variables that can be easily overridden by the user
-
-
Add a prefix to the variables in a role to indicate to which role they belong, for example postgresql_version for a variable defined in a postgresql role (this is especially useful when you are working with multiple roles at once)
-
If you use
ansible-galaxy init my-role
to initialize a role, don’t forget to remove all unused directories in the end
Templates
Include a line at the beginning of your templates to show which role generated these files. For this purpose, the variable ansible_role
is very useful; for example, your template could start like this:
1 | # this file was created from the role: {{ansible_role}}
|
2 |
***here comes your actual template*** |
-
Use the
ansible_managed
feature if you want to make it clear to other users that a specific file is generated by Ansible: you can define a variable namedansible_managed
in the Ansible configuration (ansible.cfg
)—see this minimal example of such anansible.cfg
:1 [defaults]
2 ansible_managed: Ansible managed: {file} modified
3 on %Y-%m-%d %H:%M:%S by {uid} on {host}
-
o make sure your template includes information about who generated it at what time, write this variable at the beginning of your templates:
1 # {{ansible_managed}}
2 # this file was created from the role: {{ansible_role}}
3 ***your actual template***
Conclusion
As you can see, Ansible covers a wide range of topics. There are many places where you have to be careful in order to write elegant code and save yourself more work down the road.
We hope that this serves as a good reference and can help you in your future Ansible endeavours.
If you have any questions or would like more guidance on any of these specific topics, do not hesitate to contact us.