Ansible · lesson 2 of 6
Also part of: Cryptography DevSecOps (AppSec in the pipeline)
Variable Precedence: host_vars Beats group_vars, and extra-vars Beats Everything
The same variable name, defined in three places, resolves to three different values depending on where you look — verified by actually stacking group_vars, host_vars, and -e against each other.
By SkillByExample · Updated
Ansible resolves a variable’s value by checking several sources in a fixed order — and the same variable name can genuinely mean something different depending on where you look.
group_vars vs. host_vars
# group_vars/local.yml
env_name: staging
# host_vars/localhost.yml
env_name: production
# vars_playbook.yml
---
- name: Show variable precedence
hosts: local
tasks:
- name: Print env_name
debug:
var: env_name
ansible-playbook -i inventory.ini vars_playbook.yml
TASK [Print env_name] **********************************************************
ok: [localhost] => {
"env_name": "production"
}
localhost matches both the local group (via group_vars/local.yml) and its own specific
host_vars/localhost.yml — and the host-specific value won. Host-level variables are always
more specific than group-level ones, and Ansible’s precedence order consistently favors
specific over general.
extra-vars: the override that beats everything
ansible-playbook -i inventory.ini vars_playbook.yml -e env_name=testing
TASK [Print env_name] **********************************************************
ok: [localhost] => {
"env_name": "testing"
}
-e on the command line overrode both host_vars and group_vars in one shot — extra-vars
sits at the very top of Ansible’s precedence order, above every file-based source, precisely
so a human or a CI pipeline can force a value at run time without editing anything.
Play-level vars vs. task-level vars
# task_vars_playbook.yml
---
- name: Play-level vs task-level vars
hosts: local
vars:
greeting: from the play
tasks:
- name: Print play-level greeting
debug:
var: greeting
- name: Print task-level override
vars:
greeting: from the task
debug:
var: greeting
ansible-playbook -i inventory.ini task_vars_playbook.yml
TASK [Print play-level greeting] ***********************************************
ok: [localhost] => {
"greeting": "from the play"
}
TASK [Print task-level override] ***********************************************
ok: [localhost] => {
"greeting": "from the task"
}
Same variable name, two different values within the same play — the first task sees the
play-level default, the second sees its own narrower, task-scoped override. Every task without
its own vars: block still falls back to the play’s value.
Encrypting secrets with vault
echo 'db_password: s3cret123' > secrets.yml
ansible-vault encrypt secrets.yml --vault-password-file <(echo mypassword)
cat secrets.yml
$ANSIBLE_VAULT;1.1;AES256
33316661303430643031623764373339376536353439316466663235346162336266656137383335
6562633531393738633265303233633335633066333137350a663464316235616531316131343632
35656434653733376566303831373430306533623631396434313537313263383533623861643733
6430393032613739310a323364323163376430663436333664363435373830336466333439616362
61346431353435376232666561303764663730323863666632613734323863396635
ansible-vault encrypt prints nothing on success — no confirmation message, just a clean exit.
The file on disk is now unreadable ciphertext — cat shows the vault header and encrypted
bytes, not the password. A playbook referencing secrets.yml still works normally as long as
it’s given the vault password at run time:
ansible-playbook -i inventory.ini vars_playbook.yml -e @secrets.yml --vault-password-file <(echo mypassword)
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Practice
1. Remove host_vars/localhost.yml and confirm group_vars takes over.
rm host_vars/localhost.yml
ansible-playbook -i inventory.ini vars_playbook.yml
TASK [Print env_name] **********************************************************
ok: [localhost] => {
"env_name": "staging"
}
With the more specific source gone, precedence falls back to the next one down —
group_vars/local.yml’s staging, exactly as it would have if host_vars had never existed.
2. Check a host's fully-resolved variables without running any tasks.
ansible-inventory -i inventory.ini --host localhost --vars
{
"ansible_connection": "local",
"env_name": "staging"
}
ansible-inventory --vars applies the same precedence resolution a real playbook run would,
without executing a single task — the fastest way to answer “what will this variable actually
be” before finding out mid-run. It shows every resolved variable for the host, including
connection settings from the inventory file itself, not just the ones your own playbook cares
about.
3. Try decrypting the vault file with the wrong password.
ansible-vault view secrets.yml --vault-password-file <(echo wrongpassword)
ERROR! Decryption failed (no vault secrets were found that could decrypt) on secrets.yml for secrets.yml
A wrong password fails decryption outright with a clear error — there’s no partial or silent decrypt; either the exact password is provided or the content stays inaccessible.
Next: roles — the standard directory layout for reusable, shareable Ansible content.