A Role's Public Interface

Scenario: Two roles from different teams are used in the same play. Both declare a variable called port. The second one silently wins, and a database ends up listening on a web port.

New words, in plain English

Ansible has one flat variable namespace per host. Roles do not get their own scope. Two roles that both define port are defining the same variable, and whichever loads last wins - silently.

The discipline that prevents this is unglamorous and absolute: prefix every role variable with the role name. webapp_port, webapp_workers, webapp_config_dir. It looks verbose and it eliminates an entire category of bug that is otherwise extremely hard to diagnose, because nothing in the output tells you a collision happened.

A role's interface is more than its variable names, though. It is the promise it makes: which variables it reads, which it requires, which handlers it exposes for others to notify, and what state it guarantees afterwards. README.md is the traditional place to write that down, and it is genuinely load-bearing documentation rather than decoration.

Modern Ansible can enforce the contract. meta/argument_specs.yml declares each input with a type, a description, whether it is required, and its allowed values. Ansible validates against it before the role runs, so a missing or wrong-typed input fails immediately with a clear message instead of producing a broken config file three tasks later.

Analogy: A role is a library function, and Ansible passes every argument through a single shared global. Prefixing names is the only thing standing between two libraries that both wanted a variable called port.

A worked example

# roles/webapp/defaults/main.yml - namespaced, documented
webapp_port: 8080
webapp_workers: 4
webapp_config_dir: /etc/webapp

# roles/webapp/meta/argument_specs.yml - an enforced contract
argument_specs:
  main:
    short_description: Configure the checkout web application
    options:
      webapp_port:
        type: int
        required: false
        default: 8080
        description: TCP port the application listens on
      webapp_upstream:
        type: str
        required: true
        description: Backend URL. No sensible default exists.

# Consuming it, with an override at the call site
- hosts: webservers
  roles:
    - role: webapp
      webapp_port: 8090

Overriding at the call site - - role: webapp with variables indented beneath it - is worth knowing because those values behave as role params, which sit high in the precedence order. That makes them excellent for a value genuinely specific to this one use of the role, and a poor choice for anything an operator might later want to change from group_vars.

meta/main.yml also declares dependencies:, other roles that must run first. Use them sparingly: dependencies run automatically and can surprise people who did not read the metadata. Explicit composition in the playbook is usually clearer than implicit dependency chains.

Tip: If you cannot write a role's README in a few sentences - what it needs, what it does, what it leaves behind - the role is probably doing too many unrelated things.
Goal: Put this to work in the ansible-role-refactor lab. Open /labs/ansible, pick ansible-role-refactor, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.