Parameterize Everything That Varies
A genuinely reusable role exposes every value that legitimately differs between consumers as a defaults/main.yml variable with a sensible, working default, rather than hard-coding values like a port number, package version, or file path directly into a task. The test is simple: if a second team adopting your role would need to fork it just to change one value, that value should have been a variable from the start. Good defaults also matter — a role that requires ten mandatory variables just to run at all is far less adoptable than one that works out of the box and only needs overrides for genuine customization.
Cricket analogy: It's like a franchise's central contract template having configurable clauses for match fees and bonuses rather than a fixed number baked in, so it works for both a rookie and a star player like Virat Kohli without rewriting the contract.
# roles/nginx/defaults/main.yml
nginx_listen_port: 80
nginx_worker_processes: auto
nginx_client_max_body_size: "1m"
nginx_vhosts: []
nginx_extra_conf_options: ""Idempotency and Safe Re-runs
A reusable role must be idempotent: running it repeatedly against a host that's already converged should report zero changes rather than re-applying every task blindly. This means favoring Ansible's declarative modules (like ansible.builtin.template, ansible.builtin.package, ansible.builtin.lineinfile) over shell/command tasks that execute imperative scripts, because declarative modules check current state before acting. When a shell task is genuinely unavoidable, wrap it with creates:, removes:, or an explicit changed_when condition so it doesn't falsely report 'changed' on every run.
Cricket analogy: It's like Hawk-Eye re-checking a run-out decision from the exact same replay footage every time and always reaching the identical verdict, rather than the outcome randomly changing on review.
Run ansible-playbook with --check and --diff during development, and run the role twice in a row in CI, asserting zero changed tasks on the second run, to catch idempotency regressions early.
Platform Portability and Sensible Assertions
Reusable roles often need to run across multiple operating system families, so package names, service names, and config paths should be abstracted behind OS-specific variable files loaded conditionally, for example vars/RedHat.yml versus vars/Debian.yml, included via include_vars with ansible_facts['os_family'] as the lookup key. It's equally important to fail fast and clearly: an ansible.builtin.assert task near the top of the role that checks required variables and supported platforms produces a far better error message than letting a cryptic module failure occur three tasks deep.
Cricket analogy: It's like a bowler adjusting their line and length for different pitch conditions — a green seaming pitch at Headingley versus a dry turner in Chennai — using the same fundamental skill but different tactics per surface.
Skipping an early assert for required variables and supported OS families means users discover misconfiguration only after several tasks have already run, sometimes leaving a host in a half-converged state.
- Expose every legitimately varying value as a defaults/main.yml variable with a sensible working default.
- Favor declarative modules (template, package, lineinfile) over shell/command for built-in idempotency.
- When shell/command is unavoidable, use creates:, removes:, or changed_when to prevent false 'changed' reports.
- Test idempotency in CI by running the role twice and asserting zero changes on the second run.
- Abstract OS-specific package/service/path differences into per-OS vars files loaded via include_vars.
- Add an early ansible.builtin.assert task to fail fast with a clear message on missing vars or unsupported platforms.
- A role that works out of the box with minimal required variables is far more adoptable than one demanding heavy upfront configuration.
Practice what you learned
1. What is the main test for whether a value should be exposed as a role variable?
2. Why are declarative modules like template and package preferred over shell for reusable roles?
3. How can a necessary shell/command task avoid falsely reporting 'changed' on every run?
4. What is a recommended CI check for verifying role idempotency?
Was this page helpful?
You May Also Like
Ansible Roles Explained
Learn what Ansible roles are, why they exist, and how their standard directory layout lets you package reusable, shareable automation.
Role Dependencies
Learn how to declare dependencies between Ansible roles using meta/main.yml, and understand ordering, deduplication, and pitfalls.
Testing Roles with Molecule
Learn how Molecule automates spinning up test instances, converging a role, and verifying results to give roles real automated tests.
Ansible Galaxy and Collections
Understand Ansible Galaxy as a content hub and Collections as the modern packaging format for roles, modules, and plugins.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics