The Art of Writing Ansible Roles

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:

  1. You only use the main.yml of a role to include or import tasks.
  2. All tasks must be optimized for efficiency.
  3. Data and code must be completely separate
  4. Tasks must run on an opt-in (allowlist) basis
  5. Roles must handle check mode correctly
  6. All tasks must be idempotent.
  7. Variables must have the correct namespace and be hierarchical
  8. Tags must be implemented and documented.
  9. Roles must be validated
  10. 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:

Stay informed
By subscribing to our newsletter, you declare that you agree with our privacy statement.

Ready to improve your Ansible configuration?

Michael Veentjer
Michaël Veentjer

Let’s talk


Ready to improve your Ansible configuration?

* required

By submitting this form, you confirm that you have read and understood our privacy statement.
SUE Cloud Native Solutions
Privacy overview
This website uses cookies. We use cookies to ensure that our website and services function properly, to gain insight into the use of our website, and to improve our products and marketing. For more information, please read our privacy and cookie policy.