Ansible Roles and Galaxy Best Practices

Ansible roles provide a standardized way to organize and share automation code. They encapsulate tasks, handlers, variables, and templates into reusable components that can be applied across your infrastructure. This guide covers role structure, leveraging Ansible Galaxy for community roles, managing collections, handling dependencies, testing with Molecule, and implementing best practices for production-grade automation.

Table of Contents

  1. Understanding Ansible Roles
  2. Role Directory Structure
  3. Working with Ansible Galaxy
  4. Collections and Namespacing
  5. Managing Role Dependencies
  6. Testing Roles with Molecule
  7. Best Practices and Patterns
  8. Conclusion

Understanding Ansible Roles

Roles are the primary mechanism for organizing and sharing Ansible automation. A well-designed role is self-contained, idempotent, and testable. Roles allow you to break down complex playbooks into logical, reusable components that can be versioned, shared, and integrated into larger automation workflows.

The key benefits of using roles include improved code organization, easier maintenance, better code reuse, simplified testing, and seamless sharing through Ansible Galaxy. Instead of monolithic playbooks with dozens of tasks, roles let you structure automation around specific functionality.

Role Directory Structure

A properly structured Ansible role follows a standardized directory layout. When you create a new role using ansible-galaxy role init, this structure is automatically generated:

my_role/
├── defaults/
│   └── main.yml
├── files/
├── handlers/
│   └── main.yml
├── meta/
│   ├── main.yml
│   └── runtime.yml
├── README.md
├── tasks/
│   └── main.yml
├── templates/
├── tests/
│   ├── inventory
│   └── test.yml
└── vars/
    └── main.yml

Each directory serves a specific purpose:

defaults/main.yml: Contains default variables with the lowest precedence. These can be easily overridden by users of the role. Always include sensible defaults here.

# defaults/main.yml
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_gzip_enabled: true

tasks/main.yml: The main task list executed when the role is applied. Break complex tasks into logical blocks with clear descriptions.

# tasks/main.yml
---
- name: Include OS-specific variables
  include_vars:
    file: "{{ ansible_os_family }}.yml"

- name: Install Nginx
  package:
    name: nginx
    state: present
  notify: restart nginx

- name: Create Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: yes
  notify: restart nginx

handlers/main.yml: Contains handler tasks triggered by notify directives. Handlers run once at the end of plays, ideal for service restarts.

# handlers/main.yml
---
- name: restart nginx
  systemd:
    name: nginx
    state: restarted
    enabled: yes

vars/main.yml: Role-level variables with higher precedence than defaults. Use for role-specific values.

meta/main.yml: Metadata about the role including dependencies, author info, and Galaxy metadata.

# meta/main.yml
---
galaxy_info:
  author: DevOps Team
  description: Install and configure Nginx web server
  company: Your Company
  license: MIT
  min_ansible_version: 2.9
  platforms:
    - name: Ubuntu
      versions:
        - focal
        - jammy
    - name: CentOS
      versions:
        - '8'
        - '9'
  categories:
    - web
    - system

dependencies: []

templates/: Jinja2 template files for configuration files. Templates allow dynamic content based on variables.

files/: Static files copied without modification to target systems.

tests/: Integration test playbook and inventory for basic testing before Galaxy submission.

Working with Ansible Galaxy

Ansible Galaxy is a hub for sharing Ansible roles and collections. It provides a centralized repository where you can discover, install, and publish automation code.

Installing roles from Galaxy is straightforward:

# Install a specific role
ansible-galaxy role install geerlingguy.docker

# Install multiple roles from a requirements file
ansible-galaxy role install -r requirements.yml

# Install a specific version
ansible-galaxy role install geerlingguy.docker,4.5.1

# Install to a custom path
ansible-galaxy role install -p ./roles geerlingguy.nodejs

Create a requirements.yml file to manage role dependencies across your projects:

# requirements.yml
---
roles:
  - name: geerlingguy.docker
    version: 4.5.1
    src: https://github.com/geerlingguy/ansible-role-docker

  - name: community.general
    version: ">=3.0.0"

collections:
  - name: community.docker
    version: ">=3.0.0"

  - name: ansible.posix
    version: 1.5.1

Install all requirements with:

ansible-galaxy install -r requirements.yml

To publish your role to Galaxy, you need a Galaxy namespace and GitHub account. The process involves:

# Create a properly structured role directory
ansible-galaxy role init my-role

# Tag and push to GitHub
cd my-role
git tag 1.0.0
git push origin --tags

# Import into Galaxy (via web interface)
# Then make the repository public and import it

Collections and Namespacing

Collections represent the next evolution in Ansible content organization. Collections are structured as namespace.collection and can contain roles, plugins, modules, and documentation.

Collections provide:

  • Namespace isolation preventing naming conflicts
  • Unified versioning and distribution
  • Better organization of related content
  • Support for multiple content types beyond roles

Install collections from Galaxy:

ansible-galaxy collection install community.docker
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general

# Install from requirements file
ansible-galaxy collection install -r requirements.yml

Create a collection structure:

ansible-galaxy collection init my_namespace.my_collection

This creates:

my_namespace/
└── my_collection/
    ├── galaxy.yml
    ├── plugins/
    ├── roles/
    ├── modules/
    ├── README.md
    └── docs/

Use collections in playbooks:

---
- hosts: all
  tasks:
    - name: Ensure Docker container is running
      community.docker.docker_container:
        name: myapp
        image: myapp:latest
        state: started
        ports:
          - "8080:8080"

Managing Role Dependencies

Roles often depend on other roles. Manage these dependencies in the role's meta/main.yml:

# meta/main.yml
---
dependencies:
  - name: geerlingguy.python
    version: 2.1.0
  
  - name: geerlingguy.nodejs
    version: 6.5.1
    when: install_nodejs

Dependencies are automatically installed when you install the parent role:

ansible-galaxy role install -r requirements.yml

For more control, manage dependencies explicitly in your requirements file:

---
roles:
  - name: geerlingguy.python
    version: 2.1.0
  
  - name: geerlingguy.nodejs
    version: 6.5.1
  
  - name: geerlingguy.docker
    version: 4.5.1
    dependencies:
      - geerlingguy.python

Avoid circular dependencies by structuring roles hierarchically. Keep dependencies minimal and document them clearly.

Testing Roles with Molecule

Molecule is the standard framework for testing Ansible roles. It automates the process of testing role logic against multiple operating systems and configurations.

Install Molecule:

pip install molecule molecule-docker molecule-podman

Initialize Molecule for your role:

cd my_role
molecule init scenario -d docker

This creates a molecule/ directory:

molecule/
├── default/
│   ├── converge.yml
│   ├── molecule.yml
│   ├── prepare.yml
│   ├── verify.yml
│   └── side_effect.yml
└── ...

Configure molecule/default/molecule.yml to define test scenarios:

---
driver:
  name: docker

provisioner:
  name: ansible

verifier:
  name: ansible

platforms:
  - name: ubuntu-20.04
    image: geerlingguy/docker-ubuntu2004-ansible
    pre_build_image: true
    docker_host: unix:///var/run/docker.sock

  - name: ubuntu-22.04
    image: geerlingguy/docker-ubuntu2204-ansible
    pre_build_image: true

  - name: centos-8
    image: geerlingguy/docker-centos8-ansible
    pre_build_image: true

scenario:
  name: default
  test_sequence:
    - lint
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - verify
    - destroy

The converge.yml playbook applies your role:

---
- hosts: all
  gather_facts: true
  vars:
    nginx_worker_processes: 4
  roles:
    - role: nginx

The verify.yml playbook tests the role's results:

---
- hosts: all
  gather_facts: false
  tasks:
    - name: Check Nginx is installed
      package_facts:
        manager: auto

    - name: Verify Nginx package
      assert:
        that:
          - "'nginx' in ansible_facts.packages"
        fail_msg: Nginx package not found

    - name: Check Nginx service is running
      systemd:
        name: nginx
      register: nginx_service

    - name: Verify Nginx service status
      assert:
        that:
          - nginx_service.status.ActiveState == 'active'

Run Molecule tests:

# Run complete test sequence
molecule test

# Run specific steps
molecule lint
molecule create
molecule converge
molecule verify
molecule destroy

# Run idempotence check (apply role twice)
molecule idempotence

# Debug a specific platform
molecule converge -s default
molecule login -s default

Add linting to catch style issues:

pip install ansible-lint
molecule lint

Configure .ansible-lint:

---
skip_list:
  - 'role-name'
  - 'name[casing]'

Best Practices and Patterns

Naming Conventions: Use clear, descriptive role names that indicate purpose. Prefix your Galaxy roles with your namespace to avoid conflicts.

# Good
company_name.webserver
company_name.database
company_name.monitoring

# Avoid
role1
mysite
app

Variable Organization: Use consistent variable naming. Prefix role variables with the role name to avoid collisions:

# defaults/main.yml
nginx_port: 80
nginx_user: www-data
nginx_group: www-data
nginx_enable_ssl: false

Include Pattern: Use include_tasks or import_tasks to organize complex roles:

# tasks/main.yml
---
- name: Include OS-specific tasks
  include_tasks: "{{ ansible_os_family }}.yml"

- name: Include configuration tasks
  include_tasks: configure.yml

- name: Include service tasks
  include_tasks: service.yml

Conditional Logic: Use when and conditionals to make roles flexible:

- name: Configure Nginx for SSL
  block:
    - name: Create SSL directory
      file:
        path: /etc/nginx/ssl
        state: directory
        mode: '0700'

    - name: Install SSL certificate
      copy:
        src: "files/{{ inventory_hostname }}.crt"
        dest: /etc/nginx/ssl/
      notify: restart nginx
  when: nginx_enable_ssl

Idempotence: Always write tasks that produce the same result when run multiple times:

# Good - idempotent
- name: Ensure Nginx is installed and running
  package:
    name: nginx
    state: present
  
- name: Ensure Nginx service is enabled and running
  systemd:
    name: nginx
    state: started
    enabled: yes

# Bad - not idempotent
- name: Install Nginx
  shell: apt-get install -y nginx

Error Handling: Use blocks and rescue clauses for graceful error handling:

- block:
    - name: Install package
      package:
        name: mypackage
        state: present
  rescue:
    - name: Handle installation failure
      debug:
        msg: "Failed to install package, continuing"

Documentation: Include comprehensive README.md with role description, requirements, and usage examples:

# Nginx Role

Installs and configures Nginx web server.

## Requirements

- Ansible 2.9+
- Python 2.7+ or 3.5+

## Variables

- `nginx_port`: Port to listen on (default: 80)
- `nginx_worker_processes`: Number of worker processes (default: auto)

## Example

```yaml
- hosts: webservers
  roles:
    - role: nginx
      vars:
        nginx_port: 8080

## Conclusion

Ansible roles provide the foundation for scalable, maintainable infrastructure automation. By following the standardized directory structure, leveraging Ansible Galaxy for community content, using collections for better organization, properly managing dependencies, and thoroughly testing with Molecule, you create automation that is reliable, reusable, and production-ready. Start with well-structured roles, share them through Galaxy, and build a library of automation components that accelerate infrastructure management across your organization.