Introduction
Tools such as CFEngine, Puppet, Chef, and Ansible have radically transformed infrastructure automation by providing a structured framework for organizing and sharing configuration management code, with Ansible being one of the newer tools. Ansible is widely used to automate the configuration of your IT environment and is highly flexible. Its creators describe Ansible as “open-source automation that is simple, flexible, and powerful.” However, it’s not immediately clear what makes Ansible code reliable. And while it sounds simple, it’s not always that straightforward in practice—especially in more complex environments and setups.
In Ansible, roles are a concept used to encapsulate code. Roles contain tasks, variables, handlers, and files, brought together into a modular, reusable unit that—if written correctly—is easy to share across projects and teams. Whether you’re managing a handful of servers or orchestrating complex (multi)cloud environments, roles help you write cleaner, more maintainable code. At the same time, it’s challenging to design a role that you’d call reliable and complete. In this blog, we’ll therefore provide some rules of thumb for a reliable Ansible role, so you can write better roles.
A few rules of thumb
The rules of thumb in this blog are:
- You only use the main.yml of a role to include or import tasks.
- All tasks must be optimized for efficiency.
- Data and code must be completely separate
- Tasks must run on an opt-in (allowlist) basis
- Roles must handle check mode correctly
- All tasks must be idempotent.
- Variables must have the correct namespace and be hierarchical
- Tags must be implemented and documented.
- Roles must be validated
- Roles must be documented
Please note: these are rules of thumb. Depending on the use and requirements of a role, some rules may be less relevant or additional rules may be necessary. In general, they are applicable in many situations. Below, we will elaborate on each one, with context and examples.
Example role: linux_user
To illustrate the rules of thumb, we will create a role that creates Linux users. We do this as follows:
# Creation of a basic role structure using ansible-galaxy
ansible-galaxy role init linux_user
# Navigate into the new role
cd linux_user/
# Create a supplementary tasks file
touch tasks/linux_user.yml
Copy code
Our directory structure now looks like this:
# ls ./*
./README.md
./defaults:
main.yml
./files:
./handlers:
main.yml
./meta:
main.yml
./tasks:
linux_user.yml main.yml
./templates:
./tests:
inventory test.yml
./vars:
main.yml
Copy code
This provides us with a basic structure to work with. In main.yml, we list the tasks for all user-related tasks:
# cat tasks/main.yml
---
- name: Include Linux user tasks
loop: "{{ linux_users }}"
loop_control:
loop_var: user
label: "{{ user }}"
tags:
- linux_user
- linux_user_authorized_keys
- linux_user_create
when:
- ansible_facts["os_family"] in linux_user_allowed_os_families
- linux_user_allowed_target_hostgroups | intersect(group_names)
ansible.builtin.include_tasks: linux_user.yml
Copy code
The linux_user.yml file shown above contains the actual tasks for creating users.
# cat tasks/linux_user.yml
---
- name: Create user "{{ user.name }}"
become: true
notify:
- "Send mail notification about user creation"
tags:
- linux_user
- linux_user_create
ansible.builtin.user:
# Required
name: "{{ user.name }}"
# Optional
append: "{{ user.append | default(omit) }}"
comment: "{{ user.comment | default(omit) }}"
create_home: "{{ user.create_home | default(omit) }}"
force: "{{ user.force | default(omit) }}"
group: "{{ user.group | default(omit) }}"
groups: "{{ user.groups | default(omit) }}"
hidden: "{{ user.hidden | default(omit) }}"
home: "{{ user.home | default(omit) }}"
password: "{{ user.password | default(omit) }}"
password_expire_account_disable: "{{ user.password_expire_account_disable | default(omit) }}"
password_expire_max: "{{ user.password_expire_max | default(omit) }}"
password_expire_min: "{{ user.password_expire_min | default(omit) }}"
password_expire_warn: "{{ user.password_expire_warn | default(omit) }}"
password_lock: "{{ user.password_lock | default(omit) }}"
remove: "{{ user.remove | default(omit) }}"
shell: "{{ user.shell | default(omit) }}"
state: "{{ user.state | default(omit) }}"
system: "{{ user.system | default(omit) }}"
uid: "{{ user.uid | default(omit) }}"
umask: "{{ user.umask | default(omit) }}"
update_password: "{{ user.update_password | default(omit) }}"
- name: Set authorized keys for user "{{ user.name }}"
become: true
loop: "{{ user.authorized_keys | default(linux_user_default_authorized_keys) }}"
loop_control:
loop_var: key
label: "{{ key.key }}"
tags:
- linux_user
- linux_user_authorized_keys
ansible.posix.authorized_key:
# Required
key: "{{ key.key }}"
user: "{{ user.name }}"
# Optional
comment: "{{ key.comment | default(omit) }}"
exclusive: "{{ key.exclusive | default(omit) }}"
key_options: "{{ key.key_options | default(omit) }}"
path: "{{ key.path | default(omit) }}"
state: "{{ key.state | default(omit) }}"
Copy code
In the task file above, a handler named "Send mail notification about user creation" is configured, which looks like this: