| |

LFCE 150 ๐Ÿง Ansible โ€” Roles and Reuse

โฑ๏ธ Estimated read: 11 min | |

A role is the unit of reuse in Ansible. It packages tasks, handlers, variables, templates, files, and metadata into a directory tree with a defined structure, and a playbook consumes the role by name instead of repeating the tasks inline. The role is the answer to the problem that a playbook, once it grows past a few plays, becomes a long document that is difficult to read, difficult to test, and impossible to share. The role is also the unit that Ansible Galaxy distributes, which means a role written for one project can be installed and used by another without copying files.

This chapter covers the role directory structure, the roles keyword that applies a role statically, the include_role and import_role tasks that apply a role dynamically or statically inside a play, the meta/main.yml file that declares role dependencies, and the ansible-galaxy command that creates and installs roles.

Key point: A role is a deconstructed playbook. The tasks/main.yml file contains the tasks that were previously in the play’s tasks section. The handlers/main.yml file contains the handlers. The defaults/main.yml and vars/main.yml files contain the variables. The playbook that uses the role references it by name, and the role’s file tree supplies the content.


Why roles exist

The readability problem. A playbook that configures a web server, a database, and a monitoring agent in a single file is a file with dozens of tasks and no clear boundary between the concerns. A role puts each concern in its own directory. The web server role contains only the web server tasks, the database role contains only the database tasks, and the playbook that applies them is a list of role names .

The sharing problem. A task file can be imported into a playbook, but it cannot be uploaded to Galaxy and installed by another project. A role can. The ansible-galaxy role install command downloads a role from Galaxy into the configured roles path, and the playbook references it by the namespace and name . The role is the distribution format.

The variable problem. A role has two variable directories with different precedence. defaults/main.yml has the lowest precedence of any variable source, which makes it the place for the role’s sensible defaults. vars/main.yml has a higher precedence, which makes it the place for values that should not be overridden easily. The split is deliberate: the role’s defaults can be overridden by the playbook, but the role’s internal constants cannot .


a. The role directory structure

ansible-galaxy role init creates the directory skeleton. The structure is not arbitrary; Ansible looks for specific filenames in specific subdirectories.

roles/
  webserver/
    tasks/
      main.yml
    handlers/
      main.yml
    defaults/
      main.yml
    vars/
      main.yml
    templates/
      nginx.conf.j2
    files/
      index.html
    meta/
      main.yml

The tasks/main.yml file contains the ordered list of tasks. The handlers/main.yml file contains the handlers that the tasks notify. The defaults/main.yml file contains the role’s default variables, which have the lowest precedence. The vars/main.yml file contains variables that should not be overridden. The templates/ and files/ directories contain the files that the template and copy modules reference. The meta/main.yml file contains the role’s metadata, including its dependencies .

The ansible-galaxy role init command creates this structure with placeholder main.yml files and a README.md. The role name is the directory name, and the playbook references it by that name.


b. Using a role in a playbook

The roles keyword at the play level applies the role statically. The role’s tasks run before the play’s tasks section, unless pre_tasks is used.

- name: Configure web servers
  hosts: webservers
  become: true
  roles:
    - webserver
    - common
  tasks:
    - name: Additional task after the roles
      ansible.builtin.debug:
        msg: "The roles have already run"

The role is referenced by its directory name. The roles keyword accepts a list, and the roles run in the order listed. The role’s tasks, handlers, and variables are loaded, and the role’s defaults are available to the play .

The include_role task applies a role dynamically inside the tasks section. The role’s tasks run at the point where the include_role task appears, not before the play’s tasks. The import_role task applies a role statically at parse time, which means its tasks are visible in --list-tasks and its tags are inherited by the child tasks .

- name: Apply the role dynamically
  ansible.builtin.include_role:
    name: webserver

The dynamic form is appropriate when the role’s execution depends on a previous task’s result or when the role needs to be looped. The static form is appropriate when the role is a fixed part of the play and its tasks should be visible in the task list .


c. Role dependencies

The meta/main.yml file declares the roles that must run before this role. The dependencies key is a list of role names or role specifications with parameters.

# roles/webserver/meta/main.yml
dependencies:
  - role: common
  - role: firewall
    firewall_allowed_ports:
      - 80
      - 443

The dependencies run before the role that lists them, and the pattern is recursive. If webserver depends on common, and common depends on ntp, the execution order is ntp, common, webserver .

A role runs only once per play by default, even if multiple roles list it as a dependency. The allow_duplicates: true setting in the dependent role’s meta/main.yml overrides this, but it must be set in the role that should run multiple times, not in the role that lists it . The roles keyword must be used for the dependencies to run; include_role and import_role do not trigger the dependency chain .


Complete Example Session

# ============================================
# PART 1: THE ROLE DIRECTORY
# ============================================
# roles/webserver/tasks/main.yml
- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present
  notify: Restart nginx
# ============================================
# PART 2: THE ROLE HANDLER
# ============================================
# roles/webserver/handlers/main.yml
- name: Restart nginx
  ansible.builtin.service:
    name: nginx
    state: restarted
# ============================================
# PART 3: THE ROLE DEFAULTS
# ============================================
# roles/webserver/defaults/main.yml
nginx_port: 80
nginx_server_name: localhost
# ============================================
# PART 4: THE ROLE VARS
# ============================================
# roles/webserver/vars/main.yml
nginx_package: nginx
nginx_config_dir: /etc/nginx
# ============================================
# PART 5: THE ROLE TEMPLATE
# ============================================
# roles/webserver/templates/nginx.conf.j2
server {
  listen {{ nginx_port }};
  server_name {{ nginx_server_name }};
}
# ============================================
# PART 6: THE ROLE META
# ============================================
# roles/webserver/meta/main.yml
dependencies:
  - role: common
# ============================================
# PART 7: THE PLAYBOOK USING THE ROLE
# ============================================
- name: Configure web servers
  hosts: webservers
  become: true
  roles:
    - webserver
# ============================================
# PART 8: THE DYNAMIC INCLUDE
# ============================================
- name: Apply the role conditionally
  ansible.builtin.include_role:
    name: webserver
  when: ansible_facts['os_family'] == "Debian"
# ============================================
# PART 9: THE STATIC IMPORT
# ============================================
- name: Apply the role statically
  ansible.builtin.import_role:
    name: webserver
# ============================================
# PART 10: THE GALAXY COMMANDS
# ============================================
# Create a role skeleton
ansible-galaxy role init webserver

# Install a role from Galaxy
ansible-galaxy role install geerlingguy.nginx

# Install with a specific version
ansible-galaxy role install geerlingguy.nginx,3.2.0

The ten parts covered the role directory, the handler, the defaults, the vars, the template, the meta, the playbook, the dynamic include, the static import, and the Galaxy commands.


Quick Reference

Role directory structure

DirectoryContents
tasks/main.ymlOrdered task list
handlers/main.ymlNotified handlers
defaults/main.ymlDefault variables (lowest precedence)
vars/main.ymlRole variables (higher precedence)
templates/Jinja2 templates
files/Static files
meta/main.ymlMetadata and dependencies

Role application methods

MethodSyntaxTiming
roles keywordroles: [webserver]Before the play’s tasks
include_roleTask in tasksDynamic, at the point it appears
import_roleTask in tasksStatic, at parse time

Dependencies

ItemDetail
Filemeta/main.yml
Keydependencies
ExecutionBefore the role that lists them
RecursionYes
Duplicate preventionDefault; allow_duplicates: true overrides

Galaxy commands

CommandPurpose
ansible-galaxy role init <name>Create a role skeleton
ansible-galaxy role install <namespace.name>Install from Galaxy
ansible-galaxy role install <namespace.name>,<version>Install a specific version
ansible-galaxy role listList installed roles

Best Practices

โœ… Do This:

# Use defaults for values the playbook should override
# roles/webserver/defaults/main.yml
nginx_port: 80                                              # โœ…
# Use vars for internal constants
# roles/webserver/vars/main.yml
nginx_config_dir: /etc/nginx                                # โœ…
# Declare dependencies in meta/main.yml
dependencies:
  - role: common                                            # โœ…
# Create the role skeleton with ansible-galaxy
ansible-galaxy role init webserver                          # โœ…

โŒ Don’t Do This:

# Put role tasks directly in the playbook
# The role is the reusable unit; use it                      # โŒ
# Use include_role when dependencies must run
# include_role does not trigger the dependency chain         # โŒ
# Put override-able values in vars/main.yml
# vars has high precedence; use defaults                      # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Role not foundRole not in the roles pathCheck roles_path in ansible.cfg
Dependencies do not runinclude_role used instead of rolesUse the roles keyword
Role runs only onceDuplicate role in the playUse allow_duplicates: true in meta/main.yml
Variable override ignoredVariable defined in vars/main.ymlMove it to defaults/main.yml
Template not foundTemplate not in templates/Check the filename and the directory

Real-World Examples

1. Create a role

ansible-galaxy role init webserver

2. Apply a role

roles:
  - webserver

3. Include a role dynamically

- ansible.builtin.include_role:
    name: webserver

4. Import a role statically

- ansible.builtin.import_role:
    name: webserver

5. Declare a dependency

# meta/main.yml
dependencies:
  - role: common

6. Allow duplicates

# meta/main.yml
allow_duplicates: true

7. Install from Galaxy

ansible-galaxy role install geerlingguy.nginx

8. Install a specific version

ansible-galaxy role install geerlingguy.nginx,3.2.0

9. Role defaults

# defaults/main.yml
nginx_port: 80

10. Role template

# templates/nginx.conf.j2
server { listen {{ nginx_port }}; }

Visual

The role directory

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  roles/webserver/                                            โ”‚
โ”‚    โ”œโ”€โ”€ tasks/main.yml        โ† the tasks                    โ”‚
โ”‚    โ”œโ”€โ”€ handlers/main.yml     โ† the handlers                 โ”‚
โ”‚    โ”œโ”€โ”€ defaults/main.yml     โ† overridable defaults         โ”‚
โ”‚    โ”œโ”€โ”€ vars/main.yml         โ† internal constants           โ”‚
โ”‚    โ”œโ”€โ”€ templates/            โ† Jinja2 templates             โ”‚
โ”‚    โ”œโ”€โ”€ files/                โ† static files                 โ”‚
โ”‚    โ””โ”€โ”€ meta/main.yml         โ† dependencies                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The application methods

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  roles: [webserver]                                          โ”‚
โ”‚    โ†’ runs before the play's tasks                            โ”‚
โ”‚                                                              โ”‚
โ”‚  include_role:                                               โ”‚
โ”‚    โ†’ runs at the point it appears, dynamic                   โ”‚
โ”‚                                                              โ”‚
โ”‚  import_role:                                                โ”‚
โ”‚    โ†’ parsed statically, visible in --list-tasks              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The dependency chain

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  webserver/meta/main.yml:                                    โ”‚
โ”‚    dependencies: [common]                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  common/meta/main.yml:                                       โ”‚
โ”‚    dependencies: [ntp]                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Execution order: ntp โ†’ common โ†’ webserver                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Galaxy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ansible-galaxy role init webserver                          โ”‚
โ”‚    โ†’ creates the directory skeleton                          โ”‚
โ”‚                                                              โ”‚
โ”‚  ansible-galaxy role install geerlingguy.nginx               โ”‚
โ”‚    โ†’ downloads to ~/.ansible/roles/                          โ”‚
โ”‚                                                              โ”‚
โ”‚  Playbook: roles: [geerlingguy.nginx]                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
RoleReusable bundle of tasks, handlers, vars, files
Structuretasks/, handlers/, defaults/, vars/, meta/
Createansible-galaxy role init <name>
Apply staticallyroles: keyword or import_role
Apply dynamicallyinclude_role
Dependenciesmeta/main.yml
Dependency executionBefore the role that lists them
Galaxy installansible-galaxy role install <namespace.name>

Key takeaways:

  • A role is a deconstructed playbook. The tasks, handlers, and variables that were in a playbook’s sections are moved into the role’s directory tree. The playbook that uses the role references it by name, and the role’s file tree supplies the content .
  • The roles keyword applies a role statically before the play’s tasks. The include_role task applies it dynamically at the point where the task appears, and the import_role task applies it statically at parse time. The difference matters for task visibility, tag inheritance, and dependency execution .
  • Dependencies are declared in meta/main.yml and run before the role that lists them. The dependency chain is recursive, and a role runs only once per play unless allow_duplicates: true is set in the dependent role. The roles keyword is required for the dependencies to run; include_role does not trigger the chain .
  • defaults/main.yml has the lowest variable precedence; vars/main.yml has a higher precedence. The split is what allows a role to provide sensible defaults that the playbook can override, while keeping internal constants protected from accidental override .
  • ansible-galaxy role init creates the directory skeleton. The command is the standard way to start a new role, and it saves the manual work of creating the directories and the placeholder main.yml files .
  • ansible-galaxy role install downloads a role from Galaxy. The role is installed into the configured roles path, and the playbook references it by the namespace and name. A specific version can be installed by appending the version to the role name .

Remember: A role is the reusable unit of Ansible content. It packages the tasks, handlers, variables, templates, and dependencies into a directory tree, and the playbook references it by name. The roles keyword applies it statically, include_role applies it dynamically, and import_role applies it statically inside the tasks section. The dependencies are declared in meta/main.yml and run before the role. The defaults directory is for values the playbook can override; the vars directory is for internal constants. And ansible-galaxy is the tool that creates the skeleton and installs the shared roles from Galaxy.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!