What Are Ansible Roles?

Ansible roles are a way to organize playbook content into reusable, self-contained units. Instead of writing one massive playbook, you break functionality into roles like nginx, postgresql, deploy-app — each with its own tasks, variables, templates, and files.

# Instead of a 500-line playbook, use roles:
- hosts: webservers
  roles:
    - common
    - nginx
    - deploy-app
    - monitoring

Why Use Roles?

  • Reusability — Write once, use across multiple playbooks and projects
  • Organization — Predictable directory structure, easy to navigate
  • Sharing — Publish to Ansible Galaxy or private Git repos
  • Testing — Test each role independently with Molecule
  • Collaboration — Teams work on different roles without conflicts

Role Directory Structure

roles/
└── nginx/
    ├── defaults/         # Default variables (lowest precedence)
    │   └── main.yml
    ├── vars/             # Role variables (higher precedence)
    │   └── main.yml
    ├── tasks/            # Task definitions
    │   └── main.yml
    ├── handlers/         # Handlers (triggered by notify)
    │   └── main.yml
    ├── templates/        # Jinja2 templates
    │   └── nginx.conf.j2
    ├── files/            # Static files
    │   └── index.html
    ├── meta/             # Role metadata and dependencies
    │   └── main.yml
    ├── tests/            # Test playbooks
    │   └── test.yml
    └── README.md         # Documentation

What Goes Where

DirectoryPurposeExample
defaults/Default values users can overridenginx_port: 80
vars/Internal role variables (harder to override)_nginx_user: www-data
tasks/The actual automation tasksInstall, configure, start
handlers/Actions triggered by notifyRestart nginx
templates/Jinja2 files with variablesnginx.conf.j2
files/Static files to copy as-isSSL certs, scripts
meta/Dependencies, author, licenseDepends on common role

Create a Role

Using ansible-galaxy init

ansible-galaxy init nginx

This creates the full directory structure automatically.

Manual Creation

Only create the directories you need — Ansible ignores missing ones:

mkdir -p roles/nginx/{tasks,templates,handlers,defaults}

Example: Complete nginx Role

defaults/main.yml

---
nginx_port: 80
nginx_server_name: "_"
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_root: /var/www/html
nginx_enabled: true

tasks/main.yml

---
- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Install nginx (RHEL)
  ansible.builtin.dnf:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

- name: Deploy nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: "nginx -t -c %s"
  notify: Restart nginx

- name: Ensure nginx is started and enabled
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: "{{ nginx_enabled }}"

handlers/main.yml

---
- name: Restart nginx
  ansible.builtin.service:
    name: nginx
    state: restarted

templates/nginx.conf.j2

worker_processes {{ nginx_worker_processes }};

events {
    worker_connections {{ nginx_worker_connections }};
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        listen {{ nginx_port }};
        server_name {{ nginx_server_name }};
        root {{ nginx_root }};

        location / {
            try_files $uri $uri/ =404;
        }
    }
}

meta/main.yml

---
galaxy_info:
  author: Your Name
  description: Install and configure nginx
  license: MIT
  min_ansible_version: "2.14"
  platforms:
    - name: Ubuntu
      versions: [jammy, noble]
    - name: EL
      versions: [8, 9]

dependencies:
  - role: common

Use Roles in Playbooks

Basic Usage

---
- hosts: webservers
  become: true
  roles:
    - nginx

With Variable Overrides

---
- hosts: webservers
  become: true
  roles:
    - role: nginx
      vars:
        nginx_port: 8080
        nginx_server_name: "example.com"

Multiple Roles

---
- hosts: webservers
  become: true
  roles:
    - common        # Base packages, users, SSH config
    - nginx         # Web server
    - certbot       # SSL certificates
    - deploy-app    # Application deployment
    - monitoring    # Prometheus node exporter

Conditional Roles

---
- hosts: all
  become: true
  roles:
    - role: nginx
      when: "'webservers' in group_names"
    - role: postgresql
      when: "'dbservers' in group_names"

include_role (Dynamic)

tasks:
  - name: Include role based on OS
    ansible.builtin.include_role:
      name: "{{ ansible_os_family | lower }}_base"

Role Dependencies

In meta/main.yml, declare roles that must run first:

dependencies:
  - role: common
  - role: firewall
    vars:
      firewall_allowed_ports:
        - "{{ nginx_port }}"

Dependencies run before the role's own tasks.

defaults/ vs vars/ — Variable Precedence

defaults/main.yml  →  Lowest precedence (easily overridden)
vars/main.yml      →  Higher precedence (harder to override)

Rule of thumb:

  • defaults/ → values users should customize (ports, paths, feature flags)
  • vars/ → internal values users shouldn't change (package names, paths per OS)
# defaults/main.yml — user-facing
nginx_port: 80
nginx_ssl_enabled: false

# vars/main.yml — internal
_nginx_package_name: nginx
_nginx_config_dir: /etc/nginx

Install Roles from Ansible Galaxy

# Install a single role
ansible-galaxy install geerlingguy.nginx

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

requirements.yml

---
roles:
  - name: geerlingguy.nginx
    version: "3.1.0"
  - name: geerlingguy.postgresql
    version: "3.4.0"
  - src: https://github.com/myorg/my-role.git
    scm: git
    version: main
    name: my-custom-role

Test Roles with Molecule

# Install Molecule
pip install molecule molecule-docker

# Initialize test scaffolding
cd roles/nginx
molecule init scenario --driver-name docker

# Run tests
molecule test

Conclusion

Ansible roles are the standard way to organize automation into reusable, testable components. Create roles with ansible-galaxy init, use defaults/ for user-facing variables, and share via Galaxy or Git. Every production Ansible project should use roles.