Skip to main content
RunBook Academy

AnsibleXIII · Variables and PrecedenceDesign

Names that do not collide

Intermediate⏱ ~18 minansible-playbook

What you'll learn

  • Explain why every variable on a host shares one flat namespace
  • Apply role-prefixed naming to make collisions structurally impossible
  • Identify the reserved ansible_ space and the magic variables
  • Recognise that a collision produces a wrong value, not an error

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

There is one namespace per host. Not one per role, not one per play — one flat dictionary of names, shared by every role, every task and every template that runs against that host.

A role that sets port has set port for everything. A second role that reads port gets whatever the first one left there. Nothing warns you, because from Ansible’s perspective nothing unusual happened: a variable had a value, and a template used it.

This is why role-prefixed naming is not a style preference. It is the only mechanism available for keeping two roles from interfering with each other.

The collision, demonstrated

Two roles, alpha and zulu, each with a service_name in its own vars/main.yml, run in that order in one play:

Read-only / Safeboth roles define the name
$ ansible-playbook p.yml
ok: [web-01.example.com] => {
  "msg": "ALPHA sees alpha-svc"
}
ok: [web-01.example.com] => {
  "msg": "ZULU sees zulu-svc"
}
ok: [web-01.example.com] => {
  "msg": "AFTER sees zulu-svc"
}

That looks safe — each role saw its own value. Now delete zulu’s definition, so zulu reads a name it never declared:

Read-only / Safeonly alpha defines the name
$ ansible-playbook p.yml
ok: [web-01.example.com] => {
  "msg": "ALPHA sees alpha-svc"
}
ok: [web-01.example.com] => {
  "msg": "ZULU sees alpha-svc"
}
ok: [web-01.example.com] => {
  "msg": "AFTER sees alpha-svc"
}

zulu is now configured by alpha. There is no error, no warning, and no indication in the output that anything is unusual — zulu reports a service name and proceeds to act on it.

Worse, the failure is order-dependent and conditional. If zulu runs alone, service_name is undefined and the role fails loudly at templating time, which is the good outcome. It only misbehaves when alpha happens to run first in the same play. So the role passes its own tests, passes in staging where it runs in isolation, and produces a wrong value in production where the site playbook runs both.

The rule: prefix everything a role owns

Every variable a role defines or reads is prefixed with the role’s name:

# roles/webapp/defaults/main.yml
webapp_listen_port: 80
webapp_worker_count: 1
webapp_log_level: info
webapp_config_dir: /etc/webapp

Not port, workers, log_level, config_dir. The prefix makes the collision structurally impossible: two roles cannot fight over webapp_listen_port unless one of them is deliberately reaching into the other’s namespace, which is visible in review.

It has a second benefit that matters more day to day. When you find webapp_worker_count in a group_vars file, you know immediately which role consumes it. Without the prefix, worker_count in group_vars/prod.yml could belong to any of a dozen roles, and finding out means reading all of them.

Three practical points:

Prefix with the role name, not an abbreviation. webapp_ for a role called webapp. Consistency is what makes the convention searchable; wa_ saves five characters and costs you grep.

Prefix variables the role reads, too, not just ones it defines. A role that reads a bare environment is asking to be handed something unrelated — and environment is a particularly bad choice, because it is also an Ansible keyword for setting environment variables on a task.

Estate-wide values are the deliberate exception. A genuine cross-cutting value — datacentre, environment_name, ntp_servers — belongs in group_vars under a name that is intentionally shared, and is consumed by several roles on purpose. Give those a consistent estate-level prefix of their own, so the sharing is visible: corp_, estate_, your organisation’s short name. What you want to avoid is not sharing; it is accidental sharing.

The reserved space

Some names are not yours.

ansible_* is reserved. Facts are injected into this space, as are connection settings such as ansible_host, ansible_user, ansible_port, ansible_connection and ansible_python_interpreter. Never invent a new ansible_-prefixed name; you are writing into a namespace whose future contents are decided upstream, and a future release adding your name is a collision you cannot fix from your side.

The connection variables in particular are load-bearing. ansible_host sets the address Ansible connects to, and a role that sets it — perhaps meaning “the host this application talks to” — redirects the connection itself.

Magic variables describe the run. Upstream’s list includes hostvars, groups, group_names, inventory_hostname, inventory_hostname_short, ansible_play_hosts, ansible_play_batch, ansible_playbook_python, playbook_dir, role_name, role_path, ansible_check_mode, ansible_run_tags and omit.

The documentation says of these: “These variables cannot be set directly by the user; Ansible will always override them to reflect internal state.”

Valid names

Ansible variable names may contain letters, digits and underscores, and must begin with a letter or an underscore. Nothing else is a name.

The enforcement is not uniform, which is worth knowing. set_fact rejects an invalid name outright:

Read-only / Safea hyphen is not a name
$ ansible-playbook p3.yml
[ERROR]: Task failed: Invalid variable name 'my-app-port'.
fatal: [web-01.example.com]: FAILED! => {"changed": false, "msg": "Task failed: Invalid variable name 'my-app-port'."}

That is the loud, good failure. The dangerous form is a hyphenated key in a group_vars file, which loads without complaint and then cannot be referenced as {{ my-app-port }} — Jinja parses that as subtraction of three names. Use underscores everywhere and the question never arises.

Two more traps in the same family:

  • Group names become directory and file names. A group called web-servers is fine in the inventory, but Jinja cannot reference groups.web-servers; use groups['web-servers']. Underscores in group names avoid the inconsistency.
  • Python keywords and Jinja globals. Avoid class, lambda, import, range, dict, list, namespace. Some are rejected, some shadow a Jinja global and quietly break a filter expression somewhere else in the estate.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A role passes its own tests and works in staging, but produces a wrong service name in production where it runs alongside other roles. What is the most likely cause?

  2. Q2. Which of these are genuine reasons to prefix a role variable with the role name? Select all that apply.

  3. Q3. Ansible reliably prevents you from overriding magic variables such as inventory_hostname, as the documentation states.

  4. Q4. Two Galaxy roles you cannot edit both use an unprefixed service_name, and the second one inherits the wrong value. What is the appropriate fix?

Passing score: 75%. Answers are checked in this browser.