Variables, Facts & Jinja2 Templates
Ansible
Chapter 5 · Variables, Facts & Jinja2 Templates
ansible1-3's own warn-box promised this chapter would cover variable precedence properly. It also used {{ item }} and {{ variable | default(...) }} without ever naming what that syntax actually is. Both get resolved here — along with the exact templating mechanism this course's own capstone uses to generate real config files.
Variable Precedence — Resolving Chapter 3's Own Promise
Ansible's real, official precedence order has over twenty distinct levels — more detail than is useful for getting started. The practical version most real playbooks actually need, lowest to highest:
- Role defaults (
defaults/main.yml) — the lowest priority, meant to be overridden - Inventory variables (
group_vars,host_vars, fromansible1-3) - Playbook variables (a play's own
vars:block) - Task-level variables /
set_fact - Extra vars (
-eon the command line) — always wins, no exceptions
A variable set at a higher level on this list always overrides the same variable set at a lower one. -e on the command line is the deliberate escape hatch for "just this one run, use this value, regardless of everything else" — and it always gets the final word.
Facts — Data Ansible Gathers For You
At the start of a play, Ansible automatically gathers facts about each managed host — operating system, IP addresses, memory, CPU count, hostname — unless explicitly disabled. These are available as {{ ansible_facts.xxx }}, or common ones via a shorthand like {{ ansible_distribution }} and {{ ansible_distribution_version }}.
Facts are what make a single playbook safely reusable across genuinely different hosts — the capstone's own target, Debian specifically, is exactly the kind of thing a when: condition on ansible_os_family would check for.
Jinja2 — Ansible's Templating Language
Every {{ }} used since ansible1-2 — variable substitution in a task's arguments, {{ item }} in a loop — is Jinja2, not special Ansible-only syntax. Jinja2 also supports filters: {{ variable | default('fallback') }} provides a safe fallback value when a variable might not be defined, avoiding an "undefined variable" error outright — exactly the pattern used unexplained in ansible1-4's own exercise solutions.
The template Module — Templating Real Config Files
copy transfers a file byte-for-byte, unchanged. template processes a file through Jinja2 first — substituting variables, evaluating conditionals and loops — then copies the resulting, fully rendered output.
This is exactly the mechanism ansible1-10's own capstone uses to generate real Apache vhost and security-header configuration for ws1's own hardening course — a template file, rendered per-host, replacing what was originally typed by hand.
Conditionals and Loops Inside a Template
{% if %}/{% endif %} and {% for %}/{% endfor %} are Jinja2's own template-level conditionals and loops — distinct from the when: and loop: task keywords, which control whether/how a whole task runs, not what appears inside a rendered file. A list of security headers, each with its own name and value, rendered this way is exactly the shape of content ws1's own security-headers chapter covered by hand — now generated.
| Module | What it does | Processes Jinja2? |
|---|---|---|
| copy | Transfers a file exactly as-is | No |
| template | Renders {{ }}/{% %} first, then transfers the result | Yes |
{{ my_var | default('fallback') }} substitutes 'fallback' if my_var is undefined, rather than failing the whole task. A small habit worth adopting anywhere a variable might legitimately be absent for some hosts but not others.
gather_facts: false set on a specific play is a genuine, worthwhile optimization when that play's own tasks don't actually need any fact data, not a setting to leave on everywhere purely out of habit.
Hands-On Exercises
A variable app_port is set to 8080 in group_vars/webservers.yml and to 9090 via -e app_port=9090 on the command line. State which value wins, and explain why using this chapter's own precedence order.
📄 View solutionWrite a task that installs the package "firewalld" only when ansible_os_family is "RedHat", using the when: keyword and a gathered fact.
📄 View solutionWrite a small Jinja2 template snippet (as used inside a .j2 file) that renders a line "MaxConnections {{ max_connections }}" only if ssl_enabled is true, and explain why this has to be a {% if %} block rather than the task-level when: keyword.
📄 View solutionChapter 5 Quick Reference
- Practical variable precedence, low to high: role defaults → inventory vars → playbook vars → task vars/set_fact → -e (always wins)
- Facts — auto-gathered host data (ansible_os_family, ansible_distribution, ...), usable in when: conditions
- Jinja2 is the templating language behind every {{ }} — including {{ item }} and the default filter
- copy transfers a file as-is; template renders Jinja2 first, then transfers the result
- {% if %}/{% for %} are Jinja2 template-level constructs, distinct from the task-level when:/loop: keywords
- template is the exact mechanism the capstone (ansible1-10) uses to generate ws1's own Apache/security-header config
- gather_facts: false is a real, worthwhile optimization when a play's tasks don't need fact data