Making Automation Flexible
Hardcoding values like package names, usernames, file paths, or port numbers into tasks makes your automation brittle and environment-specific. Variables decouple the logic of your automation from the specific values it operates on, allowing the same playbook to configure a development server differently from a production server.
Variables are referenced using Jinja2 double-brace syntax: {{ variable_name }} — a task that installs "{{ web_package }}" runs identically everywhere, with the actual package name supplied separately rather than hardcoded as nginx directly in the task. Variables can be defined in many places: in the playbook itself, in inventory files, in dedicated group_vars/ and host_vars/ files, in role defaults, or passed directly on the command line.
The Precedence Hierarchy
When the same variable is defined in multiple places, Ansible uses a strict precedence order to determine which value wins. Understanding this hierarchy is essential for building flexible, environment-aware automation.
Simplified hierarchy (lowest to highest priority):
| Priority | Location | Use Case |
|---|---|---|
| 1 (lowest) | roles/x/defaults/main.yml |
Sensible defaults, easily overridden |
| 2 | Inventory group_vars/all |
Org-wide defaults |
| 3 | Inventory group_vars/<group> |
Environment or role-specific values |
| 4 | Inventory host_vars/<host> |
Per-host overrides |
| 5 | Playbook vars: section |
Playbook-level values |
| 6 | roles/x/vars/main.yml |
Role internal variables (harder to override) |
| 7 | include_vars / set_fact |
Runtime-computed values |
| 8 (highest) | -e "var=value" on CLI |
Emergency overrides, CI/CD injection |
The practical pattern: define defaults in roles/x/defaults/main.yml, environment-specific values in group_vars/, and use -e only for one-off overrides or CI/CD pipeline injection (e.g., passing a build version number at deploy time).
A common mistake is defining variables in roles/x/vars/main.yml (priority 6) when you intend them to be overridable — they won't be, because vars/ has higher precedence than inventory variables. Use defaults/ for anything you want operators to be able to override.