ð ansible-roles
Use when structuring and reusing code with Ansible roles for modular, maintainable automation and configuration management.
Overview
Structure and reuse automation code with Ansible roles for modular, maintainable infrastructure.
Role Directory Structure
A well-organized Ansible role follows a standardized directory structure:
roles/
âââ webserver/
âââ README.md
âââ defaults/
â âââ main.yml
âââ files/
â âââ nginx.conf
â âââ ssl/
â âââ cert.pem
â âââ key.pem
âââ handlers/
â âââ main.yml
âââ meta/
â âââ main.yml
âââ tasks/
â âââ main.yml
â âââ install.yml
â âââ configure.yml
â âââ security.yml
âââ templates/
â âââ nginx.conf.j2
â âââ site.conf.j2
âââ tests/
â âââ inventory
â âââ test.yml
âââ vars/
âââ main.yml
Basic Role Example
tasks/main.yml
---
# Main task file for webserver role
- name: Include OS-specific variables
include_vars: "{{ ansible_os_family }}.yml"
- name: Import installation tasks
import_tasks: install.yml
tags:
- install
- webserver
- name: Import configuration tasks
import_tasks: configure.yml
tags:
- configure
- webserver
- name: Import security tasks
import_tasks: security.yml
tags:
- security
- webserver
- name: Ensure nginx is running
service:
name: "{{ nginx_service_name }}"
state: started
enabled: yes
tags:
- service
- webserver
tasks/install.yml
---
# Installation tasks for webserver role
- name: Install nginx and dependencies (Debian/Ubuntu)
apt:
name:
- nginx
- nginx-extras
- python3-passlib
state: present
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install nginx and dependencies (RedHat/CentOS)
yum:
name:
- nginx
- nginx-mod-stream
- python3-passlib
state: present
update_cache: yes
when: ansible_os_family == "RedHat"
- name: Create nginx directories
file:
path: "{{ item }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_group }}"
mode: '0755'
loop:
- "{{ nginx_conf_dir }}/sites-available"
- "{{ nginx_conf_dir }}/sites-enabled"
- "{{ nginx_log_dir }}"
- "{{ nginx_cache_dir }}"
- /var/www/html
- name: Install certbot for SSL
apt:
name: certbot
state: present
when:
- nginx_ssl_enabled
- ansible_os_family == "Debian"
tasks/configure.yml
---
# Configuration tasks for webserver role
- name: Deploy main nginx configuration
template:
src: nginx.conf.j2
dest: "{{ nginx_conf_dir }}/nginx.conf"
owner: root
group: root
mode: '0644'
validate: 'nginx -t -c %s'
backup: yes
notify:
- Reload nginx
tags:
- config
- name: Deploy site configurations
template:
src: site.conf.j2
dest: "{{ nginx_conf_dir }}/sites-available/{{ item.name }}.conf"
owner: root
group: root
mode: '0644'
validate: 'nginx -t -c {{ nginx_conf_dir }}/nginx.conf'
loop: "{{ nginx_sites }}"
when: nginx_sites is defined
notify:
- Reload nginx
- name: Enable sites
file:
src: "{{ nginx_conf_dir }}/sites-available/{{ item.name }}.conf"
dest: "{{ nginx_conf_dir }}/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ nginx_sites }}"
when:
- nginx_sites is defined
- item.enabled | default(true)
notify:
- Reload nginx
- name: Disable default site
file:
path: "{{ nginx_conf_dir }}/sites-enabled/default"
state: absent
when: nginx_disable_default_site
notify:
- Reload nginx
- name: Configure log rotation
template:
src: logrotate.j2
dest: /etc/logrotate.d/nginx
owner: root
group: root
mode: '0644'
tasks/security.yml
---
# Security tasks for webserver role
- name: Generate dhparam file
command: openssl dhparam -out {{ nginx_conf_dir }}/dhparam.pem 2048
args:
creates: "{{ nginx_conf_dir }}/dhparam.pem"
when: nginx_ssl_enabled
- name: Set secure permissions on dhparam
file:
path: "{{ nginx_conf_dir }}/dhparam.pem"
owner: root
group: root
mode: '0600'
when: nginx_ssl_enabled
- name: Configure firewall rules (ufw)
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
when:
- nginx_configure_firewall
- ansible_os_family == "Debian"
- name: Configure firewall rules (firewalld)
firewalld:
service: "{{ item }}"
permanent: yes
state: enabled
immediate: yes
loop:
- http
- https
when:
- nginx_configure_firewall
- ansible_os_family == "RedHat"
- name: Create basic auth file
htpasswd:
path: "{{ nginx_conf_dir }}/.htpasswd"
name: "{{ item.username }}"
password: "{{ item.password }}"
owner: root
group: "{{ nginx_group }}"
mode: '0640'
loop: "{{ nginx_basic_auth_users }}"
when: nginx_basic_auth_users is defined
no_log: yes
Role Variables
defaults/main.yml
---
# Default variables for webserver role
# These can be overridden in playbooks or inventory
# Package and service names
nginx_package_name: nginx
nginx_service_name: nginx
# User and group
nginx_user: www-data
nginx_group: www-data
# Directories
nginx_conf_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_cache_dir: /var/cache/nginx
nginx_pid_file: /var/run/nginx.pid
# Main configuration
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 10m
# Performance tuning
nginx_sendfile: on
nginx_tcp_nopush: on
nginx_tcp_nodelay: on
nginx_gzip: on
nginx_gzip_types:
- text/plain
- text/css
- application/json
- application/javascript
- text/xml
- application/xml
# Security
nginx_ssl_enabled: no
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
nginx_ssl_prefer_server_ciphers: on
nginx_disable_default_site: yes
nginx_configure_firewall: yes
nginx_server_tokens: off
# Sites configuration
nginx_sites: []
# Example:
# nginx_sites:
# - name: example.com
# server_name: example.com www.example.com
# root: /var/www/example.com
# enabled: yes
# ssl: yes
# Basic authentication
# nginx_basic_auth_users:
# - username: admin
# password: secretpassword
vars/main.yml
---
# Variables that should not be overridden
nginx_conf_path: "{{ nginx_conf_dir }}/nginx.conf"
nginx_error_log: "{{ nginx_log_dir }}/error.log"
nginx_access_log: "{{ nginx_log_dir }}/access.log"
# OS-specific overrides loaded via include_vars
vars/Debian.yml
---
nginx_user: www-data
nginx_group: www-data
nginx_conf_dir: /etc/nginx
nginx_service_name: nginx
vars/RedHat.yml
---
nginx_user: nginx
nginx_group: nginx
nginx_conf_dir: /etc/nginx
nginx_service_name: nginx
Role Handlers
handlers/main.yml
---
# Handlers for webserver role
- name: Reload nginx
service:
name: "{{ nginx_service_name }}"
state: reloaded
listen: "reload nginx"
- name: Restart nginx
service:
name: "{{ nginx_service_name }}"
state: restarted
listen: "restart nginx"
- name: Validate nginx config
command: nginx -t
changed_when: false
listen: "validate nginx"
- name: Reload firewall
service:
name: ufw
state: reloaded
when: ansible_os_family == "Debian"
listen: "reload firewall"
Role Templates
templates/nginx.conf.j2
# {{ ansible_managed }}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid {{ nginx_pid_file }};
events {
worker_connections {{ nginx_worker_connections }};
use epoll;
multi_accept on;
}
http {
##
# Basic Settings
##
sendfile {{ nginx_sendfile | ternary('on', 'off') }};
tcp_nopush {{ nginx_tcp_nopush | ternary('on', 'off') }};
tcp_nodelay {{ nginx_tcp_nodelay | ternary('on', 'off') }};
keepalive_timeout {{ nginx_keepalive_timeout }};
types_hash_max_size 2048;
server_tokens {{ nginx_server_tokens | ternary('on', 'off') }};
client_max_body_size {{ nginx_client_max_body_size }};
include {{ nginx_conf_dir }}/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
{% if nginx_ssl_enabled %}
ssl_protocols {{ nginx_ssl_protocols }};
ssl_ciphers {{ nginx_ssl_ciphers }};
ssl_prefer_server_ciphers {{ nginx_ssl_prefer_server_ciphers | ternary('on', 'off') }};
ssl_dhparam {{ nginx_conf_dir }}/dhparam.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
{% endif %}
##
# Logging Settings
##
access_log {{ nginx_access_log }};
error_log {{ nginx_error_log }};
##
# Gzip Settings
##
gzip {{ nginx_gzip | ternary('on', 'off') }};
{% if nginx_gzip %}
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types {{ nginx_gzip_types | join(' ') }};
{% endif %}
##
# Virtual Host Configs
##
include {{ nginx_conf_dir }}/sites-enabled/*;
}
templates/site.conf.j2
# {{ ansible_managed }}
# Site: {{ item.name }}
{% if item.ssl | default(false) %}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name {{ item.server_name }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{ item.server_name }};
ssl_certificate {{ item.ssl_cert | default('/etc/letsencrypt/live/' + item.name + '/fullchain.pem') }};
ssl_certificate_key {{ item.ssl_key | default('/etc/letsencrypt/live/' + item.name + '/privkey.pem') }};
{% else %}
server {
listen 80;
listen [::]:80;
server_name {{ item.server_name }};
{% endif %}
root {{ item.root | default('/var/www/' + item.name) }};
index {{ item.index | default('index.html index.htm') }};
{% if item.access_log is defined %}
access_log {{ item.access_log }};
{% endif %}
{% if item.error_log is defined %}
error_log {{ item.error_log }};
{% endif %}
{% if item.basic_auth | default(false) %}
auth_basic "Restricted Access";
auth_basic_user_file {{ nginx_conf_dir }}/.htpasswd;
{% endif %}
location / {
{% if item.proxy_pass is defined %}
proxy_pass {{ item.proxy_pass }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
{% else %}
try_files $uri $uri/ =404;
{% endif %}
}
{% if item.locations is defined %}
{% for location in item.locations %}
location {{ location.path }} {
{% if location.proxy_pass is defined %}
proxy_pass {{ location.proxy_pass }};
{% endif %}
{% if location.alias is defined %}
alias {{ location.alias }};
{% endif %}
{% if location.return is defined %}
return {{ location.return }};
{% endif %}
}
{% endfor %}
{% endif %}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
Role Dependencies
meta/main.yml
---
galaxy_info:
role_name: webserver
author: Your Organization
description: Install and configure nginx web server
company: Your Company
license: MIT
min_ansible_version: 2.9
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: Debian
versions:
- buster
- bullseye
- name: EL
versions:
- 7
- 8
- 9
galaxy_tags:
- web
- nginx
- webserver
- http
dependencies:
- role: common
vars:
common_packages:
- curl
- wget
- role: firewall
when: nginx_configure_firewall
allow_duplicates: no
Using Roles in Playbooks
Simple Role Usage
---
- name: Configure web servers
hosts: webservers
become: yes
roles:
- webserver
Role with Variables
---
- name: Configure web servers with custom settings
hosts: webservers
become: yes
roles:
- role: webserver
vars:
nginx_worker_processes: 4
nginx_ssl_enabled: yes
nginx_sites:
- name: example.com
server_name: example.com www.example.com
root: /var/www/example.com
ssl: yes
ssl_cert: /etc/ssl/certs/example.com.crt
ssl_key: /etc/ssl/private/example.com.key
Role with Tags
---
- name: Configure infrastructure
hosts: all
become: yes
roles:
- role: webserver
tags:
- web
- nginx
- role: database
tags:
- db
- postgres
Pre and Post Tasks
---
- name: Deploy web application
hosts: webservers
become: yes
pre_tasks:
- name: Announce deployment
debug:
msg: "Starting deployment to {{ inventory_hostname }}"
- name: Check disk space
command: df -h /
register: disk_space
changed_when: false
roles:
- webserver
- monitoring
post_tasks:
- name: Verify nginx is responding
uri:
url: http://localhost
status_code: 200
retries: 3
delay: 5
- name: Notify completion
debug:
msg: "Deployment completed successfully"
Role Inclusion Methods
Static Import
---
- name: Configure servers
hosts: all
tasks:
- name: Import webserver role
import_role:
name: webserver
vars:
nginx_worker_processes: 2
tags:
- webserver
Dynamic Include
---
- name: Configure servers based on conditions
hosts: all
tasks:
- name: Include webserver role for web servers
include_role:
name: webserver
when: "'webservers' in group_names"
- name: Include database role for db servers
include_role:
name: database
when: "'databases' in group_names"
Import with Task Files
---
- name: Custom installation workflow
hosts: webservers
tasks:
- name: Run pre-installation checks
import_role:
name: webserver
tasks_from: preflight
- name: Install nginx
import_role:
name: webserver
tasks_from: install
- name: Configure nginx
import_role:
name: webserver
tasks_from: configure
Creating Roles with Ansible Galaxy
Initialize a New Role
# Create role structure
ansible-galaxy init roles/myapp
# Create role with custom template
ansible-galaxy init --init-path roles/ myapp
# List role files
tree roles/myapp
Install Roles from Galaxy
# Install a role
ansible-galaxy install geerlingguy.nginx
# Install specific version
ansible-galaxy install geerlingguy.nginx,2.8.0
# Install from requirements file
ansible-galaxy install -r requirements.yml
requirements.yml
---
# Install from Ansible Galaxy
- name: geerlingguy.nginx
version: 2.8.0
- name: geerlingguy.postgresql
version: 3.4.0
# Install from Git repository
- name: custom-app
src: https://github.com/yourorg/ansible-role-custom-app.git
scm: git
version: main
# Install from local path
- name: internal-role
src: /path/to/roles/internal-role
Advanced Role Patterns
Role with Multiple Entry Points
# roles/webserver/tasks/main.yml
---
- name: Default task flow
import_tasks: "{{ webserver_task_flow | default('standard') }}.yml"
# roles/webserver/tasks/standard.yml
---
- import_tasks: install.yml
- import_tasks: configure.yml
- import_tasks: security.yml
# roles/webserver/tasks/minimal.yml
---
- import_tasks: install.yml
- import_tasks: configure.yml
Conditional Role Execution
---
- name: Configure servers with conditional roles
hosts: all
become: yes
roles:
- role: webserver
when:
- ansible_os_family == "Debian"
- inventory_hostname in groups['webservers']
- role: webserver-nginx
when: webserver_type == "nginx"
- role: webserver-apache
when: webserver_type == "apache"
Nested Role Dependencies
# roles/application/meta/main.yml
---
dependencies:
- role: webserver
vars:
nginx_sites:
- name: "{{ app_name }}"
server_name: "{{ app_domain }}"
proxy_pass: "http://localhost:{{ app_port }}"
- role: database
vars:
db_name: "{{ app_db_name }}"
db_user: "{{ app_db_user }}"
- role: monitoring
vars:
monitor_services:
- nginx
- "{{ app_name }}"
When to Use This Skill
Use the ansible-roles skill when you need to:
- Structure reusable automation code across multiple playbooks and projects
- Implement modular infrastructure as code with clear separation of concerns
- Share automation logic between teams or projects
- Create distributable automation packages for common infrastructure patterns
- Organize complex playbooks into manageable, testable components
- Implement role-based configuration management with variable precedence
- Build layered infrastructure with role dependencies
- Version control automation logic independently from playbooks
- Create standardized infrastructure components for consistency
- Implement security and compliance requirements through reusable roles
- Build internal automation libraries for your organization
- Contribute to or consume community automation from Ansible Galaxy
- Test infrastructure components in isolation before integration
- Implement different configurations for development, staging, and production
- Create self-documenting infrastructure through role metadata
Best Practices
- Follow standard directory structure - Use ansible-galaxy init to create roles with proper organization
- Use defaults wisely - Place overridable variables in defaults/main.yml, non-overridable in vars/main.yml
- Document thoroughly - Include comprehensive README.md with usage examples and variable documentation
- Keep roles focused - Each role should have a single, well-defined purpose
- Use role dependencies - Declare dependencies in meta/main.yml rather than in playbooks
- Tag appropriately - Apply meaningful tags to tasks for selective execution
- Implement idempotency - Ensure roles can be run multiple times safely
- Version your roles - Use semantic versioning for role releases
- Test roles independently - Include test playbooks in tests/ directory
- Use templates for configuration - Prefer Jinja2 templates over static files for flexibility
- Implement OS detection - Use ansible_os_family for cross-platform compatibility
- Secure sensitive data - Use ansible-vault for passwords and secrets in role variables
- Use handlers correctly - Only notify handlers when configuration changes
- Validate configurations - Use validate parameter in template/copy modules
- Name tasks clearly - Use descriptive names that explain what each task does
Common Pitfalls
- Overly complex roles - Trying to make one role do too many things
- Poor variable naming - Using generic names that conflict with other roles
- Missing role prefix - Not prefixing role variables with role name
- Ignoring variable precedence - Not understanding how Ansible resolves variable conflicts
- Hard-coded values - Embedding environment-specific values instead of using variables
- Missing dependencies - Not declaring role dependencies in meta/main.yml
- No validation - Deploying configurations without validation checks
- Skipping tests - Not including test playbooks or scenarios
- Poor handler design - Restarting services unnecessarily or not at all
- Missing OS support - Assuming all target systems are the same
- No backup strategy - Not backing up configurations before changes
- Ignoring idempotency - Using command/shell modules without proper guards
- Missing tags - Not tagging tasks for selective execution
- Poor template practices - Complex logic in templates instead of variables
- No version control - Not versioning roles or tracking changes