Variables Live in Many Places
Ansible accepts variables from at least 22 different sources — role defaults, group_vars, host_vars, play vars, task vars, registered facts, the CLI, and so on. The crucial thing is the precedence order — when the same variable name is defined in multiple places, which value wins?
The Eight Levels You'll Actually Use
| Level | Where | Use it for |
|---|---|---|
| 1 — extra-vars (highest) | -e var=value on the CLI | One-off overrides, CI parameters |
| 2 — task vars | vars: on a single task | Loop iteration variables |
| 3 — block vars | vars: on a block: | Shared values for a few tasks |
| 4 — role vars | roles/ | Role-internal "constants" |
| 5 — set_fact / register | Runtime, in tasks | Computed values from earlier tasks |
| 6 — play vars | vars: on the play | Most app-level config |
| 7 — inventory vars | group_vars/ + host_vars/ | Per-environment overrides |
| 8 — role defaults (lowest) | roles/ | Sensible defaults to be overridden |
💡 Tip: Rule of thumb: put sensible defaults in
defaults/main.yml (lowest priority — easy to override), put per-environment overrides in group_vars/<env>.yml, and keep secrets in Ansible Vault files referenced from group_vars. Defining Variables
YAML — variable definitions across levels
# 1. CLI extra-vars (wins everything)
# ansible-playbook site.yml -e "mysql_port=3307"
# 2. group_vars/databases.yml
mysql_port: 3306
mysql_root_password: "{{{{ vault_mysql_root_password }}}}"
# 3. host_vars/db1.example.com.yml — overrides group_vars for this one host
server_id: 1
mysql_role: primary
# 4. play-level vars: in a playbook
- hosts: databases
vars:
backup_retention_days: 7
tasks: ...
# 5. role defaults — roles/mysql/defaults/main.yml
mysql_max_connections: 200
mysql_innodb_buffer_pool_size: "1G"
Referencing Variables in Tasks
YAML — using variables
- hosts: databases
vars:
mysql_port: 3306
mysql_data_dir: /var/lib/mysql
tasks:
# Inside a string — wrap in {{{{ }}}}, always quote the whole string
- name: Render my.cnf
ansible.builtin.template:
src: my.cnf.j2
dest: "/etc/my.cnf"
vars:
mysql_pidfile: "{{{{ mysql_data_dir }}}}/mysqld.pid"
# Inside a module argument — same syntax
- name: Open MySQL port
ansible.posix.firewalld:
port: "{{{{ mysql_port }}}}/tcp"
state: enabled
# Bare expression (when the WHOLE value is the variable)
- name: Set log path
ansible.builtin.set_fact:
log_path: "/var/log/mysql/{{{{ inventory_hostname }}}}.log"
Magic Variables
Ansible exposes built-in variables with information about the run:
| Variable | What it holds |
|---|---|
inventory_hostname | The name of the current host as it appears in inventory |
ansible_host | The actual address used for SSH (may differ from inventory name) |
groups | Dictionary of all groups → list of hosts |
group_names | List of groups the current host belongs to |
hostvars | Dict of all hosts → their variables |
play_hosts | List of hosts in the current play |
ansible_facts | All gathered facts (OS, network, hardware…) |
ansible_date_time | Date / time facts from the gathering pass |
Inspect Resolved Variables
BASH — debug what a variable will be
# Show all variables for a host (huge output, pipe to less)
ansible db1 -m ansible.builtin.debug -a "var=hostvars[inventory_hostname]"
# Show one specific variable
ansible db1 -m ansible.builtin.debug -a "var=mysql_port"
# Useful at the start of a play to see what came in
- name: Dump all play vars
ansible.builtin.debug:
var: vars
⚠ Warning: Avoid changing variable values mid-play with
set_fact if a downstream task in another play needs the original. Use set_fact for new variables, not for mutating shared inputs.