Ansible Roles
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;
##
# 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(' ') }};
##
# 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 }};
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 }};
{% if item.basic_auth | default(false) %} auth_basic "Restricted Access"; auth_basic_user_file {{ nginx_conf_dir }}/.htpasswd;
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 %}
# 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 Resources Ansible Roles Documentation Ansible Galaxy Role Directory Structure Variable Precedence ansible-galaxy CLI Best Practices Role Dependencies Testing Strategies