Ansible Roles â Reusable Automation Components Guide
In this tutorial, you'll learn about Ansible Roles. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Ansible roles are self-contained units of automation that package tasks, handlers, variables, templates, and files into reusable components with a standardized directory layout.
What You'll Learn
Why It Matters
Flat playbook structures become unmanageable beyond 50 tasks. Roles organize automation into logical components â each role encapsulates everything needed to configure a specific service or capability. DodaTech maintains 30+ Ansible roles for common infrastructure components (nginx, PostgreSQL, Prometheus, filebeat, node_exporter, docker, security-hardening) shared across all teams.
Real-World Use
DodaZIP's infrastructure team develops Ansible roles in a dedicated <a href="/devops/ansible/">Ansible</a>-roles Repository. Each role is independently versioned with Git tags, tested with Molecule, and published to an internal Ansible Galaxy server. Teams consume roles via requirements.yml, pinning specific versions for stability.
flowchart TD
A[Ansible Role: nginx] --> B[defaults/main.yml]
A --> C[tasks/main.yml]
A --> D[handlers/main.yml]
A --> E[templates/]
A --> F[vars/]
A --> G[meta/main.yml]
B --> H[Default values]
C --> I[Install, configure, deploy tasks]
D --> J[reload nginx handler]
E --> K[nginx.conf.j2, vhost.conf.j2]
F --> L[OS-specific overrides]
G --> M[Dependencies & galaxy info]
style A fill:#EE0000,color:#fff
Prerequisites: Understanding of Ansible playbooks, YAML syntax, and basic Linux administration.
Role Directory Structure
ansible-roles/
nginx/
defaults/
main.yml # Lowest priority variables
vars/
main.yml # OS-specific variable overrides
tasks/
main.yml # Main task list
install.yml # Included tasks for installation
configure.yml # Included tasks for configuration
security.yml # Included tasks for hardening
handlers/
main.yml # Service handlers
templates/
nginx.conf.j2 # Main config template
vhost.conf.j2 # Virtual host template
ssl-params.conf.j2
files/
dhparam.pem # Static files for deployment
meta/
main.yml # Role metadata & dependencies
molecule/
default/
converge.yml # Test scenario
Creating a Role
# nginx/defaults/main.yml
---
nginx_version: "1.26"
nginx_user: nginx
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_server_tokens: off
nginx_remove_default_vhost: true
nginx_vhosts:
- server_name: "api.dodatech.com"
root: "/opt/dodatech/current/public"
proxy_pass: "http://localhost:3000"
ssl: true
ssl_certificate: "/etc/ssl/certs/dodatech.crt"
ssl_certificate_key: "/etc/ssl/private/dodatech.key"
nginx_vhosts_extra: {}
nginx_log_format: main
nginx_access_log: /var/log/nginx/access.log
nginx_error_log: /var/log/nginx/error.log
# nginx/vars/main.yml
---
# OS-specific package names
nginx_package: nginx
nginx_service: nginx
nginx_config_path: /etc/nginx/nginx.conf
nginx_confd_path: /etc/nginx/conf.d
nginx_sites_enabled_path: /etc/nginx/sites-enabled
# nginx/tasks/main.yml
---
- name: Include OS-specific variables
include_vars: "{{ ansible_os_family }}.yml"
when: ansible_os_family != "Debian"
- name: Install NGINX
include_tasks: install.yml
- name: Configure NGINX
include_tasks: configure.yml
- name: Deploy virtual hosts
include_tasks: vhosts.yml
- name: Apply security hardening
include_tasks: security.yml
when: nginx_harden | default(true) | bool
# nginx/tasks/install.yml
---
- name: Add NGINX repository key
apt_key:
url: "https://nginx.org/keys/nginx_signing.key"
state: present
when: ansible_os_family == "Debian"
- name: Add NGINX repository
apt_repository:
repo: "deb https://nginx.org/packages/{{ ansible_distribution | lower }}/ {{ ansible_distribution_release }} nginx"
state: present
when: ansible_os_family == "Debian"
- name: Install NGINX package
package:
name: "nginx={{ nginx_version }}*"
state: present
notify: restart nginx
# nginx/tasks/configure.yml
---
- name: Create configuration directories
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: 0755
loop:
- "{{ nginx_confd_path }}"
- /etc/nginx/ssl
- /var/log/nginx
- name: Deploy main NGINX configuration
template:
src: nginx.conf.j2
dest: "{{ nginx_config_path }}"
owner: root
group: root
mode: 0644
notify: reload nginx
- name: Remove default virtual host
file:
path: "{{ nginx_sites_enabled_path }}/default"
state: absent
when: nginx_remove_default_vhost
notify: reload nginx
# nginx/tasks/vhosts.yml
---
- name: Deploy virtual host configurations
template:
src: vhost.conf.j2
dest: "{{ nginx_confd_path }}/{{ item.server_name }}.conf"
owner: root
group: root
mode: 0644
loop: "{{ nginx_vhosts }}"
notify: reload nginx
# nginx/handlers/main.yml
---
- name: restart nginx
service:
name: "{{ nginx_service }}"
state: restarted
listen: "restart nginx"
- name: reload nginx
service:
name: "{{ nginx_service }}"
state: reloaded
listen: "reload nginx"
# nginx/meta/main.yml
---
galaxy_info:
author: DodaTech DevOps
description: NGINX web server and reverse proxy
company: DodaTech
license: MIT
min_ansible_version: "2.16"
platforms:
- name: Ubuntu
versions:
- "22.04"
- "24.04"
- name: Debian
versions:
- "11"
- "12"
galaxy_tags:
- nginx
- webserver
- proxy
- reverse-proxy
dependencies:
- role: common
vars:
common_monitoring: true
Using Roles in a Playbook
# site.yml
- name: Configure web servers
hosts: web
become: yes
vars:
nginx_vhosts:
- server_name: "www.dodatech.com"
root: "/opt/dodatech/frontend"
ssl: true
app_version: "2.5.0"
node_version: "22"
roles:
- role: common
vars:
common_ntp_enabled: true
common_firewall_enabled: true
- role: nginx
- role: nodejs
- role: app-deploy
- name: Configure database servers
hosts: database
become: yes
vars:
postgres_version: "16"
postgres_max_connections: 200
roles:
- role: common
- role: postgresql
Role Dependencies
# postgresql/meta/main.yml
dependencies:
- role: common
- role: apt-repository
vars:
repository_key: "https://www.postgresql.org/media/keys/ACCC4CF8.asc"
repository_url: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
- role: monitoring-agent
vars:
exporter_port: 9187
Testing Roles with Molecule
# nginx/molecule/default/converge.yml
---
- name: Converge
hosts: all
become: yes
vars:
nginx_vhosts:
- server_name: "test.dodatech.com"
root: "/var/www/test"
roles:
- role: nginx
# Install molecule
pip install molecule molecule-plugins[docker]
# Test the role
molecule test
# Expected output:
# --> Scenario: default
# --> Action: dependency
# --> Action: lint
# --> Action: syntax
# --> Action: create
# --> Action: prepare
# --> Action: converge
# --> Action: idempotence
# --> Action: side_effect
# --> Action: verify
# --> Action: cleanup
# --> Action: destroy
Galaxy Requirements
# requirements.yml
---
roles:
- name: dodatech.nginx
src: https://gitlab.com/dodatech/ansible-roles/nginx.git
version: v2.1.0
scm: git
- name: dodatech.postgresql
src: https://gitlab.com/dodatech/ansible-roles/postgresql.git
version: v3.0.0
scm: git
- name: geerlingguy.firewall
src: geerlingguy.firewall
version: 3.0.0
# Install roles from requirements
ansible-galaxy role install -r requirements.yml -p roles/
# Expected output:
# - extracting dodatech.nginx to roles/dodatech.nginx
# - dodatech.nginx was installed successfully
# - extracting dodatech.postgresql to roles/dodatech.postgresql
# - dodatech.postgresql was installed successfully
# - extracting geerlingguy.firewall to roles/geerlingguy.firewall
Common Configuration Mistakes
Not using
defaults/main.ymlfor configurable values: Hardcoding values intasks/main.ymlprevents user overrides. All configurable values belong indefaults/main.yml.Putting sensitive data in role defaults: Default variables are visible in version control and Galaxy metadata. Use Ansible Vault or environment-specific
group_varsfor secrets.Missing OS-specific variable files: Roles that only work on one OS break when used on different distributions. Use
vars/{{ <a href="/devops/ansible/">Ansible</a>_os_family }}.ymlandvars/{{ <a href="/devops/ansible/">Ansible</a>_distribution }}.yml.Creating monolithic task files: A single
tasks/main.ymlwith 200 tasks is unmaintainable. Break tasks into logical includes (install.yml,configure.yml,security.yml).Not testing roles with Molecule: Untested roles break silently. Molecule validates idempotence, syntax, and convergence in isolated Docker or VM environments.
Practice Questions
What is the purpose of the
defaults/directory in a role? Answer:defaults/main.ymldefines the lowest-priority variables that users can easily override. All configurable role behavior should use defaults.How do role dependencies work? Answer: Role dependencies are defined in
meta/main.yml. When a playbook includes a role, all its dependencies run first in the specified order.What is the difference between
vars/anddefaults/? Answer: Variables invars/have higher priority thandefaults/and cannot be overridden by playbook variables. Usevars/for OS-specific values you don't want changed.How does Molecule test roles? Answer: Molecule creates disposable test instances (Docker/VirtualBox/cloud), runs the role, verifies idempotence, and executes verification tests.
Challenge
Create an Ansible role for a complete monitoring stack: Prometheus server, Node Exporter, and Grafana. Include OS-specific package installation, template-based configuration files, firewall rule management, service handlers, and Molecule test scenarios for both Ubuntu 24.04 and Debian 12. Publish the role to Ansible Galaxy with proper metadata and documentation.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro