14 min read

Ansible Roles: Structuring Your Automation for Scale

Master the art of creating reusable, maintainable Ansible roles. Learn role structure, defaults vs variables, dependencies, Ansible Galaxy integration, and testing with Molecule for reliable automation at scale.

Ansible Roles: Structuring Automation for Reusability

Key Takeaways

  • Roles provide a standardised way to organise Ansible code into reusable, shareable components.
  • The role directory structure (tasks, handlers, templates, defaults, vars, meta) provides clear separation of concerns.
  • Ansible Galaxy enables sharing and consuming community roles, accelerating automation development.
  • Molecule provides a testing framework for developing and testing roles across multiple platforms.

What Are Ansible Roles?

Ansible roles are a way of organising your automation code into reusable, self-contained units. Rather than writing monolithic playbooks, roles allow you to break down complex configurations into logical, manageable components.

A role encapsulates all the tasks, handlers, variables, templates, and files needed to configure a particular piece of software or service. This modular approach makes your automation code easier to maintain, test, and share.

“Roles let you automatically load related vars, files, tasks, handlers, and other Ansible artifacts based on a known file structure.”

— Ansible Documentation

Role Directory Structure

Ansible roles follow a standardised directory structure. Each subdirectory contains a main.yml file that Ansible automatically loads.

BASH
roles/
 nginx/
     tasks/
        main.yml
     handlers/
        main.yml
     templates/
        nginx.conf.j2
     files/
        index.html
     vars/
        main.yml
     defaults/
        main.yml
     meta/
        main.yml
     tests/
        inventory
        test.yml
     README.md
DirectoryPurpose
tasks/Contains the main list of tasks to be executed by the role.
handlers/Contains handlers that may be triggered by tasks.
templates/Contains Jinja2 template files for dynamic configuration.
files/Contains static files to be copied to managed nodes.
vars/Contains variables with high precedence (not easily overridden).
defaults/Contains default variables with lowest precedence.
meta/Contains role metadata including dependencies.
library/Contains custom modules for use within the role.
tests/Contains test playbooks for the role.

Creating Your First Role

The easiest way to create a new role is using the ansible-galaxy command, which generates the standard directory structure.

BASH
# Create a new role skeleton# This creates:# This creates:This creates:
nginx/
 README.md
 defaults/
    main.yml
 files/
 handlers/
    main.yml
 meta/
    main.yml
 tasks/
    main.yml
 templates/
 tests/
    inventory
    test.yml
 vars/
     main.yml

Basic Role Tasks

roles/nginx/tasks/main.ymlyaml
# roles/nginx/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  when: ansible_os_family == "Debian"

- name: Install Nginx (RedHat)
  yum:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

- name: Copy Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
  notify: Restart Nginx

- name: Ensure Nginx is started and enabled
  service:
    name: nginx
    state: started
    enabled: yes

Using the Role in a Playbook

site.ymlyaml
# site.yml# Or with role options# Or with role options# Or with role options# Or with role options# Or with role options# Or with role options# Or with role options# Or with role options# Or with role options# Or with role optionss
- name: Configure app servers
  hosts: appservers
  become: yes
  
  roles:
    - role: nginx
      tags: ['nginx', 'webserver']
    - role: docker
      vars:
        docker_edition: 'ce'
        docker_users:
          - deploy

Defaults vs Variables

Understanding the difference between defaults and vars is crucial for creating flexible, reusable roles.

defaults/main.yml

  • • Lowest precedence
  • • Easily overridden by users
  • • Best for configurable options
  • • Should have sensible defaults

vars/main.yml

  • • Higher precedence
  • • Harder to override
  • • Best for internal constants
  • • Use for computed values
YAML
# roles/nginx/defaults/main.yml# These can be overridden by users# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# Debian default# roles/nginx/vars/main.yml# roles/nginx/vars/main.yml# Internal variables, OS-specificspecific
---
nginx_packages:
  Debian:
    - nginx
    - nginx-extras
  RedHat:
    - nginx

nginx_service_name: nginx
nginx_config_dir: /etc/nginx

Using Handlers

Handlers in roles work the same as in playbooks. They're defined in handlers/main.yml and triggered by tasks using notify.

roles/nginx/handlers/main.ymlyaml
# roles/nginx/handlers/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.yml# In tasks/main.ymlmain.yml
- name: Update Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify:
    - Validate Nginx config
    - Reload Nginx

Templates and Files

Templates use Jinja2 and allow dynamic configuration. Files are copied as-is without any processing.

roles/nginx/templates/nginx.conf.j2jinja2
# roles/nginx/templates/nginx.conf.j2
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections }};
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout {{ nginx_keepalive_timeout }};
    server_tokens {{ nginx_server_tokens }};

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log {{ nginx_log_dir }}/access.log;
    error_log {{ nginx_log_dir }}/error.log;

{% for vhost in nginx_vhosts %}
    server {
        listen {{ vhost.listen | default('80') }};
        server_name {{ vhost.server_name }};
        root {{ vhost.root }};

{% if vhost.locations is defined %}
{% for location in vhost.locations %}
        location {{ location.path }} {
            {{ location.config | indent(12) }}
        }
{% endfor %}
{% endif %}
    }
{% endfor %}
}

Role Dependencies

Roles can depend on other roles. Dependencies are defined in meta/main.yml and are executed before the role itself.

roles/wordpress/meta/main.ymlyaml
# roles/wordpress/meta/main.yml
---
galaxy_info:
  author: Ayodele Ajayi
  description: WordPress installation role
  license: MIT
  min_ansible_version: "2.10"
  platforms:
    - name: Ubuntu
      versions:
        - jammy
        - focal
    - name: Debian
      versions:
        - bullseye
  galaxy_tags:
    - wordpress
    - web
    - php

dependencies:
  - role: nginx
  - role: php
    vars:
      php_version: "8.2"
      php_extensions:
        - mysql
        - gd
        - curl
        - xml
  - role: mysql
    vars:
      mysql_databases:
        - name: wordpress
      mysql_users:
        - name: wordpress
          password: "{{ wordpress_db_password }}"
          priv: "wordpress.*:ALL"

Ansible Galaxy

Ansible Galaxy is a hub for finding, sharing, and downloading community roles. It accelerates automation development by providing pre-built, tested roles.

CommandDescription
ansible-galaxy init role_nameCreates a new role skeleton
ansible-galaxy install geerlingguy.dockerInstalls a role from Galaxy
ansible-galaxy listLists installed roles
ansible-galaxy remove role_nameRemoves an installed role
ansible-galaxy search nginxSearches for roles on Galaxy
ansible-galaxy info geerlingguy.nginxShows information about a role

Requirements File

requirements.ymlyaml
# requirements.yml# Install all requirements# Install all requirements# Install all requirements# Install all requirements# Install all requirements# Install all requirements# Install all requirements# Install all requirements# Install all requirementsuirements
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml

Testing with Molecule

Molecule is the standard testing framework for Ansible roles. It automates the process of creating instances, running your role, and verifying the results.

BASH
# Install Molecule with Docker driver# Initialise Molecule in existing role# Initialise Molecule in existing role# This creates:# This creates:# This creates:# This creates:# This creates:s:
molecule/
 default/
     converge.yml
     molecule.yml
     verify.yml

Molecule Configuration

molecule/default/molecule.ymlyaml
# molecule/default/molecule.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/converge.yml# molecule/default/verify.yml# molecule/default/verify.yml# molecule/default/verify.yml# molecule/default/verify.yml# molecule/default/verify.yml# molecule/default/verify.yml# molecule/default/verify.ymlerify.yml
---
- name: Verify
  hosts: all
  become: yes
  
  tasks:
    - name: Check Nginx is running
      service:
        name: nginx
        state: started
      check_mode: yes
      register: nginx_service
      failed_when: nginx_service.changed

    - name: Test Nginx responds
      uri:
        url: http://localhost/
        status_code: 200

Running Molecule Tests

BASH
# Full test sequence# Individual steps# Login to instance for debugging# Login to instance for debugging# Login to instance for debugging# Login to instance for debugging# Login to instance for debugging# Login to instance for debugging# Run with specific scenario# Run with specific scenarioific scenario
molecule test -s alternative-scenario

Best Practices

⚙️

Use Defaults for Flexibility

Place variables in defaults/main.yml so users can easily override them.

🎯

Keep Roles Focused

Each role should do one thing well. Compose multiple roles for complex setups.

📝

Document Everything

Include a detailed README with requirements, variables, and examples.

🏷️

Use Tags Wisely

Add tags to tasks for selective execution and debugging.

🧪

Test with Molecule

Automated testing ensures your roles work across different scenarios.

🐧

Handle OS Differences

Use conditionals or include files based on ansible_os_family.

Real-World Examples

Docker Role

roles/docker/tasks/main.ymlyaml
# roles/docker/tasks/main.yml
---
- name: Install Docker dependencies
  apt:
    name:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
    state: present

- name: Add Docker GPG key
  apt_key:
    url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
    state: present

- name: Add Docker repository
  apt_repository:
    repo: "deb https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
    state: present

- name: Install Docker
  apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-compose-plugin
    state: present

- name: Add users to docker group
  user:
    name: "{{ item }}"
    groups: docker
    append: yes
  loop: "{{ docker_users | default([]) }}"

Users Role

roles/users/tasks/main.ymlyaml
# roles/users/defaults/main.yml# roles/users/tasks/main.yml# roles/users/tasks/main.yml# roles/users/tasks/main.ymlrs/tasks/main.yml
---
- name: Create user groups
  group:
    name: "{{ item.group | default(item.name) }}"
    state: present
  loop: "{{ users }}"
  when: users_create_groups

- name: Create user accounts
  user:
    name: "{{ item.name }}"
    group: "{{ item.group | default(item.name) }}"
    groups: "{{ item.groups | default([]) }}"
    shell: "{{ item.shell | default(users_default_shell) }}"
    create_home: "{{ item.create_home | default(true) }}"
    state: present
  loop: "{{ users }}"

- name: Add SSH authorised keys
  authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ item.1 }}"
    state: present
  loop: "{{ users | subelements('ssh_keys', skip_missing=true) }}"

Troubleshooting

Common issues and solutions when working with Ansible roles.

Role not found errors

Error: “the role 'my_role' was not found”

Common causes:

  • Role not in expected path
  • Typo in role name
  • Galaxy role not installed
  • roles_path not configured correctly

Solution:

BASH
# Check where Ansible looks for roles# Install missing Galaxy roles# Install missing Galaxy roles# Specify roles path in ansible.cfg# Specify roles path in ansible.cfg# List installed roles# List installed roles# List installed roles# List installed roles# List installed roleses
ansible-galaxy list

Role dependency conflicts

Symptoms: Role runs multiple times or in wrong order

Common causes:

  • Circular dependencies between roles
  • Same role listed as dependency in multiple places
  • Version conflicts in role dependencies

Solution:

YAML
# meta/main.yml - Pin dependency versions# Prevent duplicate execution# Prevent duplicate execution# Prevent duplicate execution# Prevent duplicate execution# Use include_role for conditional execution# Use include_role for conditional executionole for conditional execution
- name: Include role conditionally
  include_role:
    name: database
  when: install_database | default(false)

Molecule test failures

Symptoms: Tests pass locally but fail in CI, or container issues

Common causes:

  • Docker not available in CI environment
  • Systemd not running in container
  • Network issues in container
  • Platform-specific failures

Solution:

YAML
# molecule.yml - Use systemd-enabled image# Skip systemd tests when not available# Skip systemd tests when not available# Skip systemd tests when not available# Skip systemd tests when not available# Skip systemd tests when not available# Debug molecule issues# Debug molecule issues# Debug molecule issues# Debug molecule issues# Debug molecule issues# Debug molecule issueses
molecule --debug test
molecule login  # Interactive debugging

Handler not running

Symptoms: Service not restarting after config changes

Common causes:

  • Handler name doesn't match notify directive
  • Handler in wrong file location
  • Task failed before handler could run
  • Handler already flushed

Solution:

YAML
# Ensure handler name matches exactly# tasks/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# handlers/main.yml# Force handler to run immediately# Force handler to run immediately# Force handler to run immediately# Listen for multiple notifications# Listen for multiple notificationsle notifications
- name: Restart app
  service:
    name: app
    state: restarted
  listen: "restart web services"

Conclusion

Ansible roles are the cornerstone of maintainable, scalable automation. By organising your code into well-structured roles, you create reusable components that can be shared across projects and teams.

Key benefits of using roles include:

  • Reusability: Write once, use across multiple projects and environments.
  • Testability: Molecule enables automated testing across platforms.
  • Shareability: Ansible Galaxy provides a platform for sharing and discovering roles.
  • Maintainability: Clear structure makes roles easy to understand and update.

Start creating roles for your common automation tasks, test them with Molecule, and consider contributing to the Ansible community through Galaxy.

Frequently Asked Questions

An Ansible role is a way of organising your automation code into reusable, self-contained units. Rather than writing monolithic playbooks, roles allow you to break down complex configurations into logical, manageable components. A role encapsulates all the tasks, handlers, variables, templates, and files needed to configure a particular piece of software or service.

The key difference is variable precedence. Variables in defaults/main.yml have the lowest precedence and are easily overridden by users, making them ideal for configurable options with sensible defaults. Variables in vars/main.yml have higher precedence and are harder to override, making them suitable for internal constants and computed values that should not be changed by role consumers.

Ansible Galaxy is a hub for finding, sharing, and downloading community roles. It accelerates automation development by providing pre-built, tested roles that you can install using the ansible-galaxy command. You can also use Galaxy to share your own roles with the community or manage role dependencies using a requirements.yml file.

Molecule is the standard testing framework for Ansible roles. It automates the process of creating instances (using Docker, Vagrant, or cloud providers), running your role against those instances, and verifying the results. Molecule enables you to test your roles across multiple platforms and scenarios, ensuring reliability before deployment.

An Ansible role follows a standardised directory structure with subdirectories for tasks (main automation logic), handlers (triggered actions like service restarts), templates (Jinja2 files for dynamic configuration), files (static files to copy), vars (high-precedence variables), defaults (low-precedence, user-configurable variables), meta (role metadata and dependencies), and tests (test playbooks).

Role dependencies are defined in the meta/main.yml file. You can specify other roles that must run before your role, including Galaxy roles with specific versions. Dependencies are automatically executed in order before the main role tasks. You can also pass variables to dependencies to customise their behaviour for your specific use case.

References & Further Reading

Related Articles

Ayodele Ajayi

Principal Engineer based in Kent, UK. Specialising in security governance, cloud architecture, and platform engineering.