Variables, facts and Jinja2 templating
Variable precedence, defaults and vars files, magic variables, gathered and custom facts, the template module, and Jinja2 filters, tests and conditionals.
Where a variable comes from
# precedence, lowest to highest (the ones that matter day to day)
# 1. role defaults roles/x/defaults/main.yml <- easiest to override
# 2. inventory group_vars/all
# 3. inventory group_vars/<group>
# 4. inventory host_vars/<host>
# 5. play vars
# 6. set_fact / registered
# 7. --extra-vars on the command line <- always wins
# roles/web/defaults/main.yml
web_port: 8080
web_workers: "{{ ansible_processor_vcpus }}"
# inventory/group_vars/web.yml
web_port: 80
# inventory/host_vars/web1.example.com.yml
web_workers: 4
# group_vars/all/vault.yml is encrypted; group_vars/all/vars.yml holds the rest
app_db_password: "{{ vault_app_db_password }}"- Put anything an operator may reasonably want to change in
defaults/main.yml. Values invars/are meant to be internal to the role. --extra-varsbeats everything, which makes it the right tool for a one-off override and a bad place for routine configuration.- Never put a secret in
group_varsunencrypted. Keep avault.ymlnext to a plainvars.ymlthat only references it. - A variable defined in two group files for the same host follows group order, which is a fragile thing to rely on — prefer host_vars.
Facts and magic variables
- name: Show the useful truth
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} runs {{ ansible_distribution }}
{{ ansible_distribution_major_version }} with
{{ ansible_memtotal_mb }} MB RAM and
{{ ansible_processor_vcpus }} vCPU
- name: Custom fact from a local file on the target
ansible.builtin.set_fact:
role: "{{ lookup('file', '/etc/app/role') }}"
- name: Facts delivered by the inventory instead of gathered
ansible.builtin.debug:
msg: "{{ ansible_host }} is in {{ datacenter | default('unknown') }}"
# magic variables worth knowing
# inventory_hostname the name as written in the inventory
# inventory_hostname_short the part before the first dot
# ansible_host the address actually connected to
# group_names groups the host belongs to
# hostvars[other_host].x another host's variable
# ansible_facts everything gathered
# ansible_check_mode true when running with --check| Fact source | Cost | Use when |
|---|---|---|
gather_facts: true | An SSH round trip per host | You need distribution or network facts |
gather_subset | Less data, less time | You only need a few fact families |
set_fact | Free, kept for the run | A computed value used later |
Custom facts in /etc/ansible/facts.d | One read | A value the host already knows |
| Inventory variables | Free | Anything you can declare up front |
Set gather_facts: false on plays that only touch the filesystem or restart a service. On a fleet of hundreds of hosts, skipping fact gathering removes minutes from every run.
Templating with Jinja2
# roles/web/templates/app.env.j2
APP_ENV={{ app_env }}
APP_PORT={{ web_port }}
APP_WORKERS={{ web_workers }}
# a value that must always be quoted
APP_NAME="{{ app_name }}"
{% if app_debug %}
APP_DEBUG=true
LOG_LEVEL=debug
{% else %}
APP_DEBUG=false
LOG_LEVEL=info
{% endif %}
# build a list from a dictionary, deterministically sorted
{% for name, value in app_options | dictsort %}
OPT_{{ name | upper }}={{ value }}
{% endfor %}
SENTRY_DSN={{ sentry_dsn | default('') }}- name: Render the environment file and validate it
ansible.builtin.template:
src: app.env.j2
dest: /etc/app/app.env
owner: app
group: app
mode: "0640"
validate: /usr/local/bin/check-env %s
notify: Restart app
- name: Demonstrate the filters you will use most
ansible.builtin.debug:
msg:
- "{{ packages | join(', ') }}"
- "{{ host_list | map('lower') | list }}"
- "{{ config | to_json }}"
- "{{ path | basename }}"
- "{{ port | int + 1 }}"
- "{{ not_found | default('fallback') }}"
- "{{ item is defined and item is not none }}"⚠️
template writes a file whose content changes only when the rendered output changes, which is what makes notify useful. copy with inline content always reports changed if the content differs, so prefer template for anything derived from variables.FAQ
Why is my variable not defined inside a role?
Check the precedence order and whether it is declared in
defaults/ at all. A variable used in a task before it is set, or spelled differently in two files, produces the classic undefined-variable error.When should I use <code>set_fact</code> instead of a variable?
When the value is computed during the run from other facts or a previous task result. Anything static belongs in defaults or inventory so it can be reviewed and overridden.
Related
Conditionals, loops and handlers in depth Roles, Vault and idempotency
Last refreshed 2026-09-18.