TT Lab
Get started
Learn Learning paths Courses

Ansible in Practice

Variable precedence: measure it before you memorise the table

Continue in TT Lab

Summary in one line

In Ansible there are twenty-two places where a variable of the same name can be planted, and a design that reduces the number of places prevents incidents more reliably than memorizing their order.

Why this is needed

"I definitely set the port to 8080 in group_vars, but 9090 shows up." Reports like this almost always have the same shape. Someone in a hurry ran the playbook with -e on the command line, and that fact was recorded nowhere. Or there was a vars/main.yml inside a role and it beat the play's vars_files.

This problem is hard because the wrong value does not show up as an error. The playbook succeeds, the files are created, only the content is different. Neither syntax checks nor linters catch it. The only thing that can catch it is actually measuring "what does this name resolve to, at this spot, right now?"

So instead of transcribing the list from the official documentation, this module measures how the winner changes as you plant the same name in one more place at a time. Someone who has measured it once does not need to memorize the table and knows what to suspect first the next time the same situation comes up.

How it works

The official documentation lists the places in twenty-two levels, from lowest to highest. The twelve places measured in the lab, from lowest to highest, are as follows.

Rank Place Character
1 Role defaults/main.yml A value offered with the message "override this from outside"
2 Inventory group_vars/all The base value for all hosts
3 Inventory group_vars/<그룹> Per-group value
4 Inventory host_vars/<호스트> Per-host value
5 Play vars: Only in this play
6 Play vars_files: A file this play reads in
7 Role vars/main.yml A role-internal value meaning "do not touch from outside"
8 Block vars: Only inside the block
9 Task vars: Only in a single task
10 set_fact A value set during execution
11 Role call parameters A value passed when calling the role
12 Command-line -e Beats everything

There are three surprising places here. In the table, the Korean placeholders in the group and host rows stand for the group name and the host name.

First, vars_files is higher than play vars:. Within the same play, the vars: written right above loses to the file read in below it. This is the opposite of the sense you get from reading the order with your eyes.

Second, a role's defaults and vars are exact opposites. They sit side by side in the same role directory, but defaults is at the very bottom and vars is far above. This difference is exactly the role author's intent — put values that may be changed from outside in defaults, and values that must not be changed in vars. If you are using someone else's role and "no matter how much I override it, it doesn't change," that value is in vars/main.yml.

Third, the narrower the scope, the higher the rank. The order play < block < task is not something to memorize but a rule. It treats whoever wrote in the narrower place as having had a more specific intent.

The tools used for measuring are also worth knowing. For contests inside the inventory, ansible-inventory --list shows the already merged result as JSON. For contests inside a play, the fastest way is to slip in a single debug task and run it.

ansible-inventory -i inventory --list   # 인벤토리 세 자리가 합쳐진 결과
ansible-inventory -i inventory --graph  # 그룹 구조

There is one trap when you give the inventory as a directory. Files with the .ini extension inside a directory are ignored by the default settings (INVENTORY_IGNORE_EXTS). A hosts.ini that worked fine when you gave a single file with -i disappears the moment it goes inside a directory. If you see no hosts at all, suspect this first.

What it looks like in the field

First, dictionaries are not merged. If you put svc_limits: {cpu: "1", memory: 1Gi} in a low place and give svc_limits: {memory: 2Gi} in a high place, the result is {memory: 2Gi}. cpu disappears. If you want to merge, you must say so explicitly with the combine filter. You can also change the global setting hash_behaviour = merge, but that setting changes the behavior of the whole repository, so even the official documentation does not recommend it.

Second, -e cannot be undone. A command-line value is higher than any place, and there is no way to override it inside the playbook. If you start using it because it is convenient, eventually "who gave what, and when" disappears from the records. Use it only for incident response, and you need a team rule to leave a record that you used it.

Third, a design that reduces the places. A defense stronger than knowing the table is reducing the places where the same name can be planted. There are three common rules — keep environment-specific values in only one place, the inventory group_vars; put only values that may be changed from outside in a role's defaults; and use -e only for exceptional situations. If you follow these three, incidents almost never happen even if you do not know the precedence table.

Fourth, vars_prompt. There is also a place that asks a person at run time (between play vars: and vars_files:). In an automation pipeline the run would stop, so do not use it in a playbook that goes into CI. This lab is graded automatically, so it is not covered.

What you will do in the next lab

You plant a single name, svc_tier, in the twelve places one at a time and measure for yourself how the winner changes. You check the three inventory places with ansible-inventory --list and the rest by running playbooks and looking at the outputs. At the end, you leave the measured order as a table a person can read, and check side by side that dictionaries are not merged and that combine merges them.

References