Ansible Lesson 23 – Roles Introduction| Dataplexa
Section II · Lesson 23

Roles Introduction

In this lesson

What roles are Directory structure Using roles in playbooks Role variables Role dependencies

An Ansible role is a standardised, reusable unit of automation that packages tasks, variables, handlers, templates, files, and defaults together in a defined directory structure. Where a playbook is a single-file script that runs tasks against hosts, a role is a self-contained module of automation logic that any playbook can call by name. Roles exist to solve the maintainability problem that grows with every playbook: rather than copying Nginx tasks across five different playbooks and keeping them in sync, you write an nginx role once and call it from any playbook that needs it. The role is the standard unit of sharing in the Ansible community — every role on Ansible Galaxy follows the same structure, which means you can understand any role you download within minutes of opening it.

Roles vs Flat Playbooks

Roles are not a replacement for playbooks — they work alongside them. The playbook decides which hosts to target and which roles to apply. The role provides the reusable logic. This separation of concerns is what makes large Ansible projects maintainable.

Flat playbook (no roles)
All tasks inline — hundreds of lines in one file
Nginx tasks duplicated across multiple playbooks
Difficult to test in isolation
Cannot be shared with other teams or projects
Role-based playbook
Playbook is a short list of role names — logic lives in roles
Write the Nginx role once, call it from every playbook that needs it
Each role tested independently with Molecule (Lesson 10)
Publishable to Ansible Galaxy for community sharing

Role Directory Structure

Every Ansible role follows the same directory layout. Ansible loads each subdirectory automatically — you never need to include or import files from within a role explicitly. This convention is what makes every role immediately navigable.

roles/nginx/ tasks/main.yml Entry point — all role tasks handlers/main.yml Handlers for this role's tasks defaults/main.yml Default variable values (lowest precedence) vars/main.yml Internal variables (high precedence) templates/ Jinja2 templates (.j2 files) files/ Static files to copy meta/main.yml Role metadata & dependencies tests/ Test inventory + playbook README.md Required for Galaxy publishing Only tasks/ is required All other directories are optional

Role subdirectory reference

tasks/main.yml The entry point for all role tasks. Ansible executes this file automatically when the role is called. The only required subdirectory.
defaults/main.yml Default variable values — the lowest precedence of any variable source. These are the values the role uses if nothing else overrides them. Always define role defaults here, not in vars/.
vars/main.yml Internal role variables with high precedence — values here override group_vars and inventory variables. Use for constants the role defines internally, not for values the caller should be able to override.
handlers/main.yml Handlers scoped to this role. They can be notified by any task in the role and run at the end of the play like regular handlers.
templates/ Jinja2 template files. The template module in role tasks can reference these files by name alone — no path needed, Ansible finds them automatically.
files/ Static files for the copy module. Like templates, they can be referenced by name alone — no path required.
meta/main.yml Role metadata — author, description, supported platforms, and most importantly, role dependencies. Ansible reads this to automatically apply dependency roles before the current role runs.

The IKEA Flatpack Analogy

An Ansible role is like an IKEA flatpack. The box contains everything needed to assemble one specific piece of furniture — parts, hardware, and instructions — in a standardised package that anyone can pick up and use. You do not need to know how it was designed; you just follow the standard assembly process. The role's directory structure is the box layout — always the same, always in the same place, immediately recognisable to anyone who has used a role before. And like IKEA, you can customise it with optional accessories (defaults variables) without modifying the core design.

Creating a Role

The fastest way to create the role skeleton is with ansible-galaxy role init. It generates the full directory structure with placeholder files, saving you the manual work and ensuring nothing is missed.

# Create a new role skeleton in the current directory
ansible-galaxy role init nginx

# Create inside a specific roles/ directory
ansible-galaxy role init --init-path roles/ nginx
- Role nginx was created successfully

roles/nginx/
├── README.md
├── defaults/
│   └── main.yml
├── files/
├── handlers/
│   └── main.yml
├── meta/
│   └── main.yml
├── tasks/
│   └── main.yml
├── templates/
├── tests/
│   ├── inventory
│   └── test.yml
└── vars/
    └── main.yml

What just happened?

Ansible created the entire role skeleton with empty placeholder files in every standard directory. You now have a valid, importable role structure — start by editing tasks/main.yml to add your tasks, then defaults/main.yml to define configurable defaults. All other files are optional and can be left empty or deleted if unused.

Annotated roles/nginx/tasks/main.yml

---
# roles/nginx/tasks/main.yml
# No 'hosts:' or 'become:' here — those are set in the playbook that calls this role

- name: Install Nginx
  ansible.builtin.package:
    name: "{{ nginx_package }}"    # variable from defaults/main.yml
    state: present

- name: Deploy Nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2             # found automatically in templates/ — no path needed
    dest: /etc/nginx/nginx.conf
    mode: "0644"
  notify: Reload Nginx             # handler in handlers/main.yml

- name: Deploy virtual host configurations
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{ item.name }}.conf"
    mode: "0644"
  loop: "{{ nginx_vhosts }}"       # variable from defaults/main.yml
  notify: Reload Nginx

- name: Ensure Nginx is started and enabled
  ansible.builtin.service:
    name: "{{ nginx_package }}"
    state: started
    enabled: true

Annotated roles/nginx/defaults/main.yml

---
# roles/nginx/defaults/main.yml
# These are the LOWEST priority variables — callers can override every one of these.

nginx_package: nginx
nginx_user: www-data
nginx_worker_processes: "{{ ansible_processor_vcpus | default(1) }}"
nginx_worker_connections: 1024
nginx_port: 80
nginx_vhosts:
  - name: default
    server_name: "{{ ansible_fqdn }}"
    root: /var/www/html

roles/nginx/handlers/main.yml

---
# roles/nginx/handlers/main.yml
- name: Reload Nginx
  ansible.builtin.service:
    name: "{{ nginx_package }}"
    state: reloaded

- name: Restart Nginx
  ansible.builtin.service:
    name: "{{ nginx_package }}"
    state: restarted

Using Roles in Playbooks

There are three ways to use roles in a playbook, each with different timing and scoping behaviour. Understanding which to use in which context prevents subtle ordering bugs.

Method 1

roles: key

Classic method. Roles listed under roles: run before any tasks in the play. Most common and recommended for standard provisioning.

Method 2

include_role

Dynamic import at runtime. The role is loaded when the task executes — allows conditional role inclusion using when:.

Method 3

import_role

Static import at parse time. Like include_role but processed before the play runs — supports tags on role tasks.

---
# Method 1 — roles: key (most common)
- name: Configure web servers
  hosts: webservers
  become: true
  roles:
    - nginx                    # calls roles/nginx/tasks/main.yml
    - role: nodejs             # alternative verbose form
      vars:
        node_version: "20"     # override role defaults for this play
    - role: app_deploy
      tags: deploy             # tag the entire role

---
# Method 2 — include_role (dynamic — supports when:)
- name: Configure servers conditionally
  hosts: all
  become: true
  tasks:
    - name: Install Nginx on web servers only
      ansible.builtin.include_role:
        name: nginx
      when: "'webservers' in group_names"

    - name: Install PostgreSQL on DB servers only
      ansible.builtin.include_role:
        name: postgresql
      when: "'databases' in group_names"

---
# Method 3 — import_role (static — tags work on role tasks)
- name: Deploy application
  hosts: appservers
  become: true
  tasks:
    - name: Run application role
      ansible.builtin.import_role:
        name: app_deploy
      tags: deploy              # tags propagate into role tasks

Role Dependencies

A role can declare that it depends on other roles using the meta/main.yml file. Ansible automatically runs dependency roles first — before the dependent role executes. This is how complex roles express their prerequisites without requiring the playbook author to know the exact order.

# roles/app_deploy/meta/main.yml
---
galaxy_info:
  author: yourname
  description: Deploys the application to web servers
  license: MIT
  min_ansible_version: "2.14"
  platforms:
    - name: Ubuntu
      versions: ["22.04", "20.04"]

dependencies:
  # These roles run BEFORE app_deploy, automatically
  - role: nginx                  # ensure Nginx is installed first
    vars:
      nginx_port: 8080           # override nginx defaults for this dependency

  - role: common                 # common base configuration
    # no variable overrides — use common's defaults

When a playbook calls app_deploy, Ansible resolves the full dependency tree first: it runs common, then nginx (with the port override), then app_deploy. The playbook author does not need to know or specify this order — the role itself encodes its prerequisites.

Put Overridable Variables in defaults/, Not vars/

This is the most common role authoring mistake. Variables in vars/main.yml have very high precedence — they override group_vars, host_vars, and play vars:. If you put your role's configurable options in vars/, callers cannot override them through normal variable mechanisms. Always put configurable options in defaults/main.yml — which has the lowest precedence of any variable source — and reserve vars/ for internal constants that should never be overridden.

Key Takeaways

Only tasks/main.yml is required — all other subdirectories are optional. Start with just tasks and defaults, and add handlers, templates, and files as the role grows.
Always define configurable options in defaults/ — these have the lowest variable precedence and can be overridden by any caller through inventory variables, play vars, or extra vars.
Templates and files are referenced by name alone inside a role — Ansible automatically looks in the role's templates/ and files/ directories, so no path prefix is needed in task src: parameters.
Role dependencies in meta/main.yml run automatically — Ansible resolves the full dependency tree before running the role, so the playbook author does not need to specify the correct order manually.
Use include_role for conditional role application — the roles: key in a play cannot use when: conditions, but include_role inside a task list can, enabling roles to be applied only to the hosts that need them.

Teacher's Note

Run ansible-galaxy role init nginx, move your Lesson 13 Nginx tasks into roles/nginx/tasks/main.yml, move your template into roles/nginx/templates/, and call the role from a two-line playbook. The moment the playbook shrinks from 40 lines to 5, roles stop being a concept and become the obvious way to write Ansible.

Practice Questions

1. What command generates a new role skeleton with all standard subdirectories and placeholder files?



2. In which role file should you define configurable variables that callers can override through inventory or play variables?



3. In which role file do you declare the other roles that must run before this role executes?



Quiz

1. What is the key difference between variables defined in defaults/main.yml and vars/main.yml?


2. A role's meta/main.yml lists two dependency roles. What happens when a playbook calls this role?


3. You want to apply the nginx role only to hosts in the webservers group, even though your play targets all. How do you achieve this?


Up Next · Lesson 24

Creating and Using Roles

Build a complete, production-ready role from scratch — structuring tasks, vars, templates, and handlers — then call it from a multi-role site.yml that provisions a full server stack.