Ansible roles are the primary unit of reusable automation. A role is a structured directory tree that encapsulates a complete, self-contained piece of configuration work — a 'nginx' role configures Nginx, a 'postgresql' role installs and configures PostgreSQL, a 'hardening' role applies OS security baselines. Roles replace the flat, monolithic playbook structure (where all tasks for all components live in one file) with a component-oriented architecture where each role owns its domain completely: its tasks, its default variable values, its templates, its files, its handlers, and its dependency declarations. A 600-line playbook for a three-tier application becomes three focused roles of 200 lines each, independently testable, independently versionable, and reusable across different projects. The role structure is standardised by Ansible conventions — the directory layout is the same for every role, making it immediately navigable by any Ansible practitioner.
The distinction between role defaults and role vars is one of the most important (and most misunderstood) aspects of role design. Defaults ('defaults/main.yml') define the lowest-priority variable values — they are the factory settings that callers are explicitly expected to override. Vars ('vars/main.yml') define high-priority internal variables that are part of the role's implementation and are not intended to be overridden by callers. A correctly designed role puts all user-facing configuration in defaults/ (where callers can easily customise) and puts internal constants in vars/ (where callers should not need to touch them). This distinction enables roles to be opinionated about their internal implementation while remaining flexible about their external interface — exactly the same principle as well-designed Terraform modules with typed variables and hidden locals.
The Standard Role Directory Layout
Ansible enforces a conventional directory structure for roles. Under the role's root directory: 'tasks/main.yml' is the entry point — where all tasks are defined or imported; 'defaults/main.yml' holds low-priority variable defaults that callers can override; 'vars/main.yml' holds high-priority internal variables; 'handlers/main.yml' defines handlers used by the role's tasks; 'templates/' holds Jinja2 templates; 'files/' holds static files that are copied to managed hosts; 'meta/main.yml' declares the role's author, license, minimum Ansible version, and inter-role dependencies (roles that must run before this one); 'tests/' holds test playbooks for the role; and 'README.md' documents the role. Not all directories are required — a simple role might only have tasks/, defaults/ and templates/. Creating roles with 'ansible-galaxy role init role_name' generates the full skeleton structure, which is the recommended starting point even for simple roles.
#!/bin/bash
# Create a production role with full structure
ansible-galaxy role init cricket_nginx
# Review the generated structure
tree cricket_nginx/
# cricket_nginx/
# ├── defaults/
# │ └── main.yml # Overridable defaults
# ├── files/ # Static files for copy module
# ├── handlers/
# │ └── main.yml # Handlers triggered by notify
# ├── meta/
# │ └── main.yml # Role metadata and dependencies
# ├── README.md
# ├── tasks/
# │ └── main.yml # All tasks (entry point)
# ├── templates/ # Jinja2 templates
# ├── tests/
# │ ├── inventory
# │ └── test.yml
# └── vars/
# └── main.yml # Internal vars (high priority)
# ── defaults/main.yml — user-facing configuration ────────────────────────────
cat > cricket_nginx/defaults/main.yml << 'DEFAULTS'
---
# Nginx service configuration — callers should override these
nginx_port: 80
nginx_ssl_port: 443
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 50m
nginx_server_name: "{{ inventory_hostname }}"
# SSL settings
nginx_ssl_enabled: false
nginx_ssl_cert_path: /etc/ssl/certs
nginx_ssl_key_path: /etc/ssl/private
# Application upstream
nginx_upstream_host: 127.0.0.1
nginx_upstream_port: 8080
nginx_upstream_keepalive: 32
# Logging
nginx_access_log: /var/log/nginx/access.log
nginx_error_log: /var/log/nginx/error.log
nginx_log_level: warn
DEFAULTS
# ── vars/main.yml — internal implementation constants ────────────────────────
cat > cricket_nginx/vars/main.yml << 'VARS'
---
# Internal variables — not intended for caller override
nginx_config_dir: /etc/nginx
nginx_sites_dir: /etc/nginx/conf.d
nginx_service_name: nginx
nginx_packages:
RedHat: nginx
Debian: nginx
VARS
# ── meta/main.yml — dependencies and metadata ────────────────────────────────
cat > cricket_nginx/meta/main.yml << 'META'
---
galaxy_info:
author: nagarajarao
description: Nginx reverse proxy for Cricket Analytics Platform
company: Sri Hayavadhana Info-Tech
license: MIT
min_ansible_version: '2.15'
platforms:
- name: Ubuntu
versions: ['22.04', '24.04']
- name: Amazon
versions: ['2023']
categories:
- web
dependencies:
# This role requires firewall to be configured before Nginx starts
- role: cricket_firewall
vars:
firewall_allowed_ports:
- { port: 80, proto: tcp }
- { port: 443, proto: tcp }
META# tasks/main.yml — Role task entry point
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars:
file: '{{ ansible_os_family }}.yml'
failed_when: false # Skip if no OS-specific vars file exists
- name: Install Nginx packages
ansible.builtin.package:
name: '{{ nginx_packages[ansible_os_family] | default("nginx") }}'
state: present
tags: [nginx, packages]
- name: Create Nginx configuration directories
ansible.builtin.file:
path: '{{ item }}'
state: directory
owner: root
group: root
mode: '0755'
loop:
- '{{ nginx_config_dir }}'
- '{{ nginx_sites_dir }}'
tags: [nginx]
- name: Deploy main Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: '{{ nginx_config_dir }}/nginx.conf'
mode: '0644'
validate: 'nginx -t -c %s'
backup: true
notify: Reload Nginx
tags: [nginx]
- name: Deploy virtual host configuration
ansible.builtin.template:
src: vhost.conf.j2
dest: '{{ nginx_sites_dir }}/cricket-api.conf'
mode: '0644'
notify: Reload Nginx
tags: [nginx]
- name: Enable and start Nginx
ansible.builtin.service:
name: '{{ nginx_service_name }}'
enabled: true
state: started
tags: [nginx]
# handlers/main.yml
# - name: Reload Nginx
# ansible.builtin.service:
# name: '{{ nginx_service_name }}'
# state: reloaded
# Using the role in a playbook:
# - name: Configure API servers
# hosts: api_servers
# roles:
# - role: cricket_nginx
# vars:
# nginx_upstream_port: 8080
# nginx_ssl_enabled: true
# nginx_server_name: api.cricket-analytics.io