Ansible / Intermediate: reuse, secrets, and scoped validation / Lesson 9
Package reusable behavior as role interfaces
Design role inputs, defaults, tasks, handlers, dependencies, and static versus dynamic reuse with explicit validation.
On this page
Lesson goals
- Explain role layout and public input ownership.
- Distinguish imports from dynamic includes.
- Validate role arguments and prevent hidden cross-role coupling.
A role is a reusable contract, not only a directory
Roles organize tasks, handlers, defaults, variables, files, templates, and metadata through a known layout. Reuse becomes reliable when the role declares required inputs, optional defaults, supported platforms, expected effects, and acceptance requirements. Avoid a role that silently consumes generic variables from unrelated playbooks. Prefix public inputs and distinguish values the caller may override from internal implementation state. A role should not become a bag of every task an organization has ever needed. Cohesive responsibility and a small input schema make it easier to review scope, version changes, and failure behavior.
Defaults and role variables have different override intent
Role defaults normally provide low-precedence customization points, while role vars can override sources a caller expected to control. Choose placement from ownership, not convenience. Validate required keys and types rather than giving every input a harmless-looking default. Dependency roles can introduce additional tasks, variables, and handlers, so the visible role name alone does not describe the full execution graph. Document dependencies and their versions. Repeated role invocation and duplicate handling also have documented rules; do not assume a dependency executes once per call or that repeating a role is always equivalent to repeating its task list.
Static import and dynamic include evaluate at different times
Static imports expand reusable content during playbook processing, while dynamic includes bring content into the runtime flow under their rules. This affects when variables and conditions are evaluated, which tasks are visible to listing/tag workflows, and how inherited keywords apply. A condition on imported content can apply to individual expanded tasks, while an include condition controls whether the inclusion occurs; details must be checked for the chosen construct. Choose one from the intended execution contract rather than replacing names mechanically. Test tags, variables, and handler availability in the actual reuse context.
Make a dynamic role call’s parameters visible
A short include_role task can express which role is requested and which data is supplied. It does not prove the role exists, its dependency chain is safe, or its arguments are supported. The following fictional lab role is illustrative and is never loaded. A real project should use documented role argument validation where available and maintain an interface changelog. Keep validation independent from host-changing tasks so malformed input is rejected before effects. Public variable exposure and tag inheritance should be chosen explicitly using supported options rather than inferred from one observed run.
- name: Illustrate a role interface
hosts: lab_nodes
gather_facts: false
tasks:
- name: Request the fictional report role
ansible.builtin.include_role:
name: lab_report
vars:
lab_report_settings:
workers: 2
mode: review
lab_report is a fictional role used to explain the interface. YAML parsing does not resolve or execute it, install dependencies, or contact a target.
Test the data interface separately from role execution
An offline schema assertion can check required keys and type constraints in a synthetic input object. It is not the role argument-spec engine and cannot validate plugin loading or managed-node behavior. Use it as a small illustration of what the real role must require. In a later authorized disposable test, exercise valid input, missing input, unsupported platform, first convergence, second-run stability, and recovery after partial failure. Keep role tests focused on public behavior rather than internal task counts, so refactoring a well-defined implementation does not break an otherwise valid contract.
settings = {'workers': 2, 'mode': 'review'}
required = {'workers', 'mode'}
assert required <= settings.keys()
assert type(settings['workers']) is int and settings['workers'] > 0
assert settings['mode'] in {'review', 'archive'}
print('synthetic_role_contract=valid')
synthetic_role_contract=valid
The assertions apply only to this Python fixture. They do not invoke Ansible role validation, discover a role directory, or execute tasks.
Common errors and fixes
A role consumes unrelated generic variables
- Why it happens
- Its public input boundary was never defined.
- How to fix it
- Introduce namespaced inputs and documented defaults.
- Prevent it next time
- Test the role in more than one caller context.
include and import are treated as synonyms
- Why it happens
- Processing time and inheritance behavior were ignored.
- How to fix it
- Choose and test the intended reuse contract.
- Prevent it next time
- Include tag and conditional cases in reuse tests.
A role name hides unreviewed dependencies
- Why it happens
- The transitive execution graph was omitted.
- How to fix it
- Inventory dependency roles, versions, and effects.
- Prevent it next time
- Review dependency changes as scope changes.
Check your understanding
1. Why should an override-friendly input usually live in role defaults rather than an arbitrary high-precedence source?
Show worked solution
The location should reflect caller ownership. A low-precedence default lets an explicit project input customize behavior under documented rules. A stronger role variable can unexpectedly defeat that intention and create hidden coupling.
2. What does passing an offline role schema test leave unproved?
Show worked solution
It leaves role loading, dependency resolution, templating, module compatibility, target effects, idempotence, and application health unproved. Report the data-contract check accurately and plan those additional checks only in a separately authorized environment.
Key takeaways
- Design roles as small versioned public interfaces.
- Review timing, inheritance, and transitive dependencies explicitly.