Skip to main content
RunBook Academy

AnsibleXIII · Variables and PrecedenceDesign

-e always wins, and that is the problem

Intermediate⏱ ~19 minansible-playbook

What you'll learn

  • Explain why extra vars outrank every other source and what that costs
  • Recognise that -e applies uniformly to every host, erasing per-host exceptions
  • Distinguish the narrow legitimate uses of -e from habitual use
  • Constrain extra vars so a run remains reproducible from the repository

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.

Entry 22 of the precedence table has a parenthetical that no other entry has: “always win precedence”.

That is not a ranking, it is a guarantee. There is nothing you can put in a role, an inventory, a playbook or a task that will survive an -e on the command line. Extra vars are the one source with no counter.

For an emergency that is exactly right. At 03:00, with a bad value baked into a role you cannot safely edit and a fleet in a degraded state, being able to overrule the entire configuration system with one flag is a genuinely good property. Keep it, and be glad it exists.

The trouble is that the same property makes -e the path of least resistance for every awkward configuration problem, and an estate that takes that path stops being describable by its own repository.

What the guarantee actually buys

Recall the estate from lesson 1. web-02 has a deliberate host_vars exception — two workers, because it runs on older two-core hardware pending replacement. Every other web host takes the production policy of sixteen.

Read-only / Safethe designed state
$ ansible-playbook site.yml
ok: [web-01.example.com] => {
  "msg": "port=443 workers=16 log=warn"
}
ok: [web-02.example.com] => {
  "msg": "port=443 workers=2 log=warn"
}

Now one operator adds one flag:

Configuration changethe same play with an extra var
$ ansible-playbook site.yml -e webapp_worker_count=99
ok: [web-01.example.com] => {
  "msg": "port=443 workers=99 log=warn"
}
ok: [web-02.example.com] => {
  "msg": "port=443 workers=99 log=warn"
}

This is the property people miss. Extra vars are not host-scoped. They are a single global binding applied identically to every host in the run, and they sit above host_vars, so every deliberate per-host exception in the estate is erased at once.

The documented reason web-02 was different — two physical cores — did not stop being true. The mechanism that recorded it was simply overruled, silently, by a flag typed for an unrelated reason.

What two years of -e looks like

The decay is gradual and each step is locally reasonable.

Month one. A value is wrong in production during an incident. Someone runs the play with -e to fix it now and open a pull request in the morning. Correct call.

Month three. The pull request was never opened. The -e is in a runbook, as a copy-paste line. It works, so nobody revisits it.

Month eight. The scheduled job has three -e flags. They are in the cron entry, or the CI job definition, or a wrapper shell script — in any case, not in the Ansible repository. The repository now describes a configuration nobody actually runs.

Month eighteen. A new engineer reads group_vars/prod.yml, makes a change, tests it, and ships it. Nothing happens in production, because an -e three layers up in a Jenkins job has been overriding that variable since before they joined. They lose a day, and their conclusion is that the estate is haunted.

Month twenty-four. Four specific properties are gone:

  • The repository no longer describes what will happen. Reading it gives you a prediction that is wrong in ways you cannot enumerate.
  • Runs are not reproducible. The same playbook, the same inventory and the same commit produce different results depending on who invokes it and from where.
  • Review reviews the wrong thing. A pull request against group_vars/ is approved on the assumption it takes effect. Sometimes it does not, and review has no way to tell.
  • The precedence table predicts nothing. It correctly says -e wins. It cannot tell you which -e flags exist, because they are not in the repository at all.

The last one is the deepest. Everything else in this part is a technique for reasoning about a repository. Heavy -e use removes the repository from the reasoning.

The legitimate uses

There are real ones. They share a shape: the value is genuinely not a property of the infrastructure, and it genuinely differs per invocation.

A one-off operational parameter. A maintenance window, a target release for one deployment, a batch identifier. Something that describes this run rather than these hosts.

ansible-playbook deploy.yml --limit web --extra-vars 'app_release=2026.08.3'

A CI-injected build identifier. The pipeline knows the artefact version; the repository cannot. This is -e doing exactly what it is for, and the value is recorded in the pipeline run.

A genuine emergency override. The role is wrong, the fix is not ready, and hosts need correcting now. Use it, then open the pull request — and make the pull request the definition of done, not the successful run.

Everything on that list is a value that varies per run. If a value varies per host or per environment, it belongs in the inventory; that is what the inventory is.

Constraining the ones you keep

Four rules make the difference between controlled use and decay.

Never in a scheduled job. This is the important one. A cron entry, a systemd timer, an AWX job template or a CI pipeline that carries -e has moved part of the configuration outside the repository permanently. If a scheduled run needs a non-default value, put it in the inventory and let the schedule invoke the play with no overrides.

Record it where the change is recorded. An -e used during an incident goes in the change ticket, verbatim, including the full command line. This is the only artefact that will let anyone reconstruct what happened.

Prefer a file to a flag for anything structured. -e accepts @filename and reads YAML or JSON from it:

ansible-playbook deploy.yml --extra-vars '@release-2026-08-3.yml'

The file can be committed, reviewed and referenced by the change ticket. It is still entry 22 and still overrides everything, but it is at least an artefact rather than a shell history entry.

Make its use visible in the run. A play can report what it was given and refuse to proceed on values that should never be overridden:

- name: refuse an override of the safety floor
  ansible.builtin.assert:
    that: webapp_tls_min_version is version('1.2', '>=')
    fail_msg: >-
      webapp_tls_min_version was overridden to
      {{ webapp_tls_min_version }}, below the estate floor of 1.2

An assertion is the one thing -e cannot bypass: the override lands, and then the play refuses to act on it. Part XXIV covers this pattern as a general guardrail.

The test

One question tells you whether your -e use is under control:

Given the repository, the inventory and the commit hash, can you predict what a scheduled run will do?

If yes, extra vars are being used the way they should be — occasionally, deliberately, for values that describe a single run.

If the honest answer is “not without checking the job definition”, then the configuration lives in two places, only one of which is reviewed, and the estate has the property the next lesson’s incident depends on.

Knowledge check

Knowledge check · 4 questions

  1. Q1. An estate has a host_vars file limiting web-02 to two workers because it runs on two-core hardware. An operator runs the play with -e webapp_worker_count=99. What happens to web-02?

  2. Q2. A scheduled job that carries -e flags has moved part of the configuration outside the repository, where it is not reviewed.

  3. Q3. Which of these are defensible uses of --extra-vars? Select all that apply.

  4. Q4. Which single test best tells you whether extra vars are under control in an estate?

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