Role Variables and Interfaces¶
What You'll Learn¶
- How to design
defaults/main.ymlas a role's public, documented interface - How to validate inputs automatically with
meta/argument_specs.yml - Why role variables need a prefix, and how collisions actually happen
- How to pass variables into a role, and how scope differs between
roles:,import_role, andinclude_role
Why This Exists¶
A well-designed role has a clear "contract" — what a consumer must set, what's optional with sensible defaults, and what's genuinely internal — the same discipline as designing a function signature, applied to defaults/main.yml.
Mental Model¶
Treat a role like a function.
defaults/main.ymlis the parameter list with default values.meta/argument_specs.ymlis the type signature and validation.vars/main.ymlholds private constants. The README is the docstring. If a consumer has to readtasks/main.ymlto use the role, the interface is missing.
roles/nginx/
├── README.md # documented interface
├── defaults/main.yml # public inputs, all prefixed nginx_
├── vars/main.yml # internal constants
├── meta/
│ ├── main.yml # dependencies, platforms
│ └── argument_specs.yml # input validation
└── tasks/main.yml
Designing defaults/main.yml¶
---
# Port nginx listens on for HTTP. Set to 0 to disable the HTTP listener.
nginx_http_port: 80
# Enable TLS. When true, nginx_tls_certificate and nginx_tls_key are required.
nginx_tls_enabled: false
nginx_tls_certificate: ""
nginx_tls_key: ""
# Worker processes. "auto" uses one per CPU core.
nginx_worker_processes: auto
# Virtual hosts to create. Each item: {name, server_names, upstream}.
nginx_vhosts: []
# Remove the distribution's default site.
nginx_remove_default_site: true
Rules that keep this file useful:
- Every variable the role reads from outside has a default here, even if it's empty.
- A comment on each explains purpose and valid values.
- Values are safe for a first run in a lab, not tuned for one team's production.
Validating Inputs: meta/argument_specs.yml¶
Since ansible-core 2.11, a role can declare its inputs. Ansible validates them automatically before any of the role's tasks run:
---
argument_specs:
main:
short_description: Install and configure nginx
options:
nginx_http_port:
type: int
default: 80
description: HTTP listen port; 0 disables the HTTP listener.
nginx_tls_enabled:
type: bool
default: false
nginx_tls_certificate:
type: str
description: Path to the certificate on the managed node.
nginx_tls_key:
type: str
no_log: true
nginx_worker_processes:
type: raw
default: auto
nginx_vhosts:
type: list
elements: dict
default: []
options:
name:
type: str
required: true
server_names:
type: list
elements: str
required: true
upstream:
type: str
required: true
Pass a string where a list is expected, and the play fails immediately with a message naming the parameter. ansible-doc can't read role specs directly, but ansible-navigator doc and collection documentation tooling can, so this also becomes your generated reference.
Cross-field rules (such as "TLS enabled requires a certificate") still need an assertion:
- name: Validate TLS settings
ansible.builtin.assert:
that:
- nginx_tls_certificate | length > 0
- nginx_tls_key | length > 0
fail_msg: "nginx_tls_enabled is true, so nginx_tls_certificate and nginx_tls_key must be set"
quiet: true
when: nginx_tls_enabled
Why the Prefix Matters¶
Most variables share one namespace per host. Two roles in the same play that both read port will read the same value:
roles:
- role: app # reads `port`, defaults to 8080
- role: metrics # also reads `port`, defaults to 9100
If inventory sets port: 8080 for the app, the metrics role silently listens on 8080 too. Prefix every variable with the role name — app_port, metrics_port — and the collision can't happen. ansible-lint's var-naming rule enforces this.
Internal helper variables deserve a prefix as well, often with a double underscore to signal "private": __nginx_package_name.
Passing Variables Into a Role¶
# 1. From inventory — the usual way for environment-specific values
# group_vars/production.yml: nginx_worker_processes: 8
# 2. At the point of use, as role parameters
- hosts: web
roles:
- role: nginx
vars:
nginx_tls_enabled: true
nginx_tls_certificate: /etc/ssl/shop.pem
nginx_tls_key: /etc/ssl/private/shop.key
# 3. Dynamically
- hosts: web
tasks:
- name: Configure nginx for the admin site
ansible.builtin.include_role:
name: nginx
vars:
nginx_http_port: 8081
Role parameters and include_role vars: sit high in Variable Precedence — above inventory — so they're the right way to give the same role different inputs in one play.
Using a role twice¶
- hosts: web
roles:
- role: nginx_vhost
vars: { nginx_vhost_name: shop, nginx_vhost_port: 8080 }
- role: nginx_vhost
vars: { nginx_vhost_name: admin, nginx_vhost_port: 8081 }
Ansible runs a role only once per play if its parameters are identical. Different vars: count as different parameters, so both run. (allow_duplicates: true in meta/main.yml forces repeats even with identical parameters.)
Scope: Do Role Variables Leak?¶
| How the role is used | Its defaults/vars visible to later tasks in the play? |
|---|---|
roles: at play level |
Yes |
import_role |
Yes |
include_role |
No, unless public: true |
Relying on leaked role variables in later tasks couples plays to role internals. If other tasks need a value, expose it deliberately with set_fact or include_role public: true, and document it.
Documenting the Interface¶
## Required variables
None for plain HTTP. With `nginx_tls_enabled: true`:
| Variable | Description |
|---|---|
| `nginx_tls_certificate` | Certificate path on the managed node |
| `nginx_tls_key` | Private key path on the managed node |
## Optional variables
See `defaults/main.yml`; every variable is documented there.
## Outputs
Sets no facts. Notifies the `Reload nginx` handler when configuration changes.
## Example
- hosts: web
roles:
- role: nginx
vars:
nginx_vhosts:
- { name: shop, server_names: [shop.example.com], upstream: "127.0.0.1:8080" }
Common Mistakes¶
- Unnamespaced role variables (
portinstead ofnginx_port) colliding with another role's variable of the same name. - A role with no documented interface — consumers have to read
tasks/main.ymlto discover what's configurable. - Putting configurable values in
vars/main.yml, where inventory can't override them. - Reading a variable in tasks that has no default, so the role fails with "undefined" instead of a clear validation message.
- Depending on variables that leaked from a previously run role.
Interview Questions¶
- Why namespace role variables instead of using short, generic names?
- How would you document a role's required vs. optional variables for other teams to consume it safely?
- What does
meta/argument_specs.ymlgive you that assertions in tasks don't? - How do you apply the same role twice in one play with different inputs?
Next¶
Continue to Production Role Design.