Open-Source Wikis

/

Ansible

/

Primitives

/

Role and handler

ansible/ansible

Role and handler

A role bundles tasks, handlers, defaults, vars, files, templates, and meta into a reusable unit. It's both a directory structure on disk and a Python class in lib/ansible/playbook/role/. Handlers are tasks that run only on notify:.

Role on disk

roles/myrole/
├── defaults/
│   └── main.yml          # Lowest-precedence variables
├── files/                # Files copyable via copy/template
├── handlers/
│   └── main.yml          # Handler tasks
├── meta/
│   └── main.yml          # Role dependencies, supported platforms, argument_specs
├── tasks/
│   └── main.yml          # The role's task list
├── templates/            # Jinja templates
├── tests/
└── vars/
    └── main.yml          # Higher-precedence variables (vs defaults)

Roles live in:

  • A play's roles: list (relative to the playbook's roles/ directory or ANSIBLE_ROLES_PATH).
  • An installed collection: <collection>/roles/<name>/.

Role classes

File Class Lines
lib/ansible/playbook/role/__init__.py Role 674
lib/ansible/playbook/role/include.py RoleInclude 90
lib/ansible/playbook/role/definition.py RoleDefinition 230
lib/ansible/playbook/role/metadata.py RoleMetadata 130
lib/ansible/playbook/role/requirement.py RoleRequirement 70
lib/ansible/playbook/role_include.py IncludeRole 240

The class hierarchy is unusually deep because roles support many usage patterns:

  • Static roles: list at play level (Role, applied at play compile time).
  • Dynamic include_role: (IncludeRole, applied at task time).
  • Static import_role: (compiled in early but as a task, not a play-level role).
  • Role dependencies declared in another role's meta/main.yml.

RoleDefinition is the parser; it reads the YAML entry (string short-form or dict long-form), looks up the role on disk, and produces a Role instance.

How a role gets compiled into the play

graph TD
    PLAY[play.roles list] --> RD[RoleDefinition.load]
    RD --> FIND[Locate role on disk]
    FIND --> META[Read meta/main.yml]
    META --> DEPS[Recursively load dependencies]
    DEPS --> ROLE[Role instance]
    ROLE --> COMPILE[Role.compile]
    COMPILE --> TASKS[Tasks list with role context]
    TASKS --> SPLICE[Splice into play's compiled task list]

Role.compile() returns the role's tasks as a list of Block objects with the role context attached:

  • Each task gets a _role reference back to the role.
  • The role's vars/main.yml and defaults/main.yml are made available as low-precedence vars on the host scope.
  • Handlers from handlers/main.yml are added to the play's handler list.

The _role reference matters because lookup('file', 'foo.txt') from inside a role searches role-relative paths first (<role>/files/foo.txt).

Role dependencies

meta/main.yml can declare role dependencies:

dependencies:
  - role: shared.base
  - role: webserver
    vars:
      webserver_port: 8080

When loading a role, Role._load_dependencies() recurses through these and produces a flattened, deduplicated list of all role tasks in dependency order. Cycles are detected and rejected.

role.allow_duplicates: yes in meta/main.yml lets a role be applied multiple times (with different vars per application) — useful for "create a user" or "open a port" roles.

Argument specs for roles

Modern roles can declare an argument spec for include_role/import_role invocations:

# meta/argument_specs.yml
argument_specs:
  main:
    short_description: Configure the webserver
    options:
      port:
        type: int
        required: true
      hostname:
        type: str
        default: localhost

The validation runs via the validate_argument_spec action plugin (lib/ansible/plugins/action/validate_argument_spec.py). When you include_role: name=foo, the argument spec for main is checked first; if invalid, the role doesn't run.

This is the same machinery as module argument validation, but applied to role parameters.

Handlers

Handler is a Task subclass:

class Handler(Task):
    listen = FieldAttribute(isa='list', default=list)

The only addition is listen:. Handlers go into the play's handlers list (separate from regular tasks). They run when:

  • A regular task notify:s them by name or by listen: topic.
  • The strategy reaches an explicit meta: flush_handlers task.
  • The play ends (all queued handlers fire).

Handler ordering

Handler order is the order they're declared in handlers: (or in roles[*].handlers/main.yml), not the order they were notified. If you have:

handlers:
  - name: handler-a
    debug: msg=A
  - name: handler-b
    debug: msg=B

then notify: [handler-b, handler-a] runs handler-a first.

This is sometimes surprising but consistent: handler ordering is your responsibility at definition time.

Handler triggering

Strategy.process_notifications() (lib/ansible/plugins/strategy/__init__.py) tracks per-host notification lists. When a regular task fires notify:, the handler name is added to the host's pending list. At flush time, the strategy iterates the play's handler list, and for each handler that any host has pending, it dispatches that handler against those hosts.

listen: for cross-handler triggering

handlers:
  - name: reload nginx
    listen: web-changed
    ...
  - name: reload haproxy
    listen: web-changed
    ...

notify: web-changed triggers both handlers. The listen: value is also a topic name; multiple handlers can listen on the same topic, and one task can notify: either a handler name or a listen: topic.

force_handlers

By default, if a play has any failed hosts, handlers don't fire on those hosts (the host's task stream stopped). Setting force_handlers: true on the play (or globally with ANSIBLE_FORCE_HANDLERS) makes handlers fire even after a failure — useful for "make sure cleanup happens regardless".

meta: flush_handlers

Forces all pending notifications to fire immediately rather than waiting for play-end. Used to ensure config changes are applied before subsequent tasks depend on them.

include_role and import_role

Two ways to use a role inside a task list rather than at play level:

Keyword Static/dynamic Equivalent of
import_role static Inserting the role's tasks at the import location, at parse time
include_role dynamic Resolving the role at task execution time, with the current vars

Use include_role when the role name itself is templated (name: "{{ chosen_role }}") or when you want the role to be re-evaluated per loop iteration. Use import_role when you want consistent tag-application and parse-time behavior.

IncludeRole (the action plugin equivalent) lives in lib/ansible/playbook/role_include.py and the action plugin lib/ansible/plugins/action/include_role.py (actually, this dispatches through the strategy plugin's IncludedFile machinery rather than a regular action). Same for import_role — it's parsed as a task at parse time but resolved by lib/ansible/playbook/helpers.py.

Integration points

  • Imported by: lib/ansible/playbook/play.py:Play._load_roles(), the strategy plugins (for IncludeRole), the validate_argument_spec action.
  • Imports: lib/ansible/parsing/dataloader.py, lib/ansible/plugins/loader.py, lib/ansible/playbook/role/{definition,metadata,include,requirement}.py.

Entry points for modification

  • A new role-level keyword — declare it in Role or RoleMetadata as appropriate. Update the role schema docs.
  • Argument-spec semanticslib/ansible/plugins/action/validate_argument_spec.py and the role's meta/argument_specs.yml parser.
  • Handler firing logiclib/ansible/plugins/strategy/__init__.py:StrategyBase. This code is intricate; existing tests in test/units/plugins/strategy/ are the best reference.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Role and handler – Ansible wiki | Factory