Variables, Facts, Variable Precedence & Custom Facts

Master the 22-level Ansible variable precedence hierarchy, system facts (setup module), custom local facts (/etc/ansible/facts.d/), and variable naming standards.

intermediate 25 min lesson hands-on task included

Variables in Ansible allow playbooks to adapt dynamically across environments (dev vs. staging vs. production) without hardcoding values into task logic.

However, because variables can be defined in over 20 different places, understanding Variable Precedence and System Facts is vital for predicting variable evaluation and debugging scope conflicts.


Topic 1: System Facts (ansible_facts) & Performance Optimization

JINJA2 TEMPLATING ENGINE & VARIABLE PRECEDENCE HIERARCHY TEMPLATE MODULE (templates/nginx.conf.j2 → /etc/nginx/nginx.conf) Jinja2 Source Template (.j2) {{ ansible_managed | comment }} worker_processes {{ nginx_workers }}; listen {{ http_port | default(80) }}; {% for server in upstream_servers %} Rendered File on Target (/etc/nginx/...) # Ansible managed: Do NOT edit manually worker_processes 4; listen 80; server 10.0.1.10:8080; VARIABLE PRECEDENCE LADDER (22 LEVELS — HIGHEST OVERRIDES LOWEST) 1. Extra Vars ansible-playbook -e "v=1" Highest Precedence 2. Task Vars vars: inside task Mid Precedence 3. Host Facts ansible_facts Mid Precedence 4. group_vars group_vars/all.yml Mid Precedence 5. Role Defaults roles/x/defaults/main.yml Lowest Precedence RED HAT BEST PRACTICE: PREFIX ROLE VARIABLES Always prefix role variables with the role name (e.g., `nginx_worker_processes` instead of `workers`) to prevent namespace collisions!
Ansible Variable Precedence Ladder: Extra vars (-e) override task vars, host facts, group_vars, and role defaults.

When a play begins, Ansible automatically executes an implicit initial task called Gathering Facts using the setup module. Facts are system properties discovered dynamically on managed nodes:

  • ansible_facts['distribution']: OS Distribution (e.g., Ubuntu, RedHat, Debian).
  • ansible_facts['distribution_major_version']: OS Major Version (e.g., 22, 9).
  • ansible_facts['default_ipv4']['address']: Primary IP address.
  • ansible_facts['memtotal_mb']: Total RAM in megabytes.
  • ansible_facts['processor_vcpus']: Total CPU core count.
  • ansible_facts['os_family']: OS Family (Debian, RedHat, Archlinux).

Disabling Fact Gathering to Optimize Speed:

Gathering facts takes 2–5 seconds per host over SSH. If a playbook only performs simple API calls or static deployments that do not rely on system facts, disable fact gathering to drastically speed up execution:

- name: High Performance API Orchestration
  hosts: all
  gather_facts: false             # Disables implicit setup module task
  tasks:
    - name: Trigger webhook endpoint
      ansible.builtin.uri:
        url: https://api.acme.com/trigger
        method: POST

Topic 2: Custom Local Facts (/etc/ansible/facts.d/*.fact)

In addition to standard hardware/OS facts, managed nodes can define Custom Local Facts.

Any file placed in /etc/ansible/facts.d/ ending in .fact (INI or JSON format) or executable scripts returning JSON are automatically gathered by the setup module and exposed inside ansible_facts.ansible_local:

# Managed Node File: /etc/ansible/facts.d/datacenter.fact
[location]
tier = production
datacenter_code = us-east-dc1
rack_id = A-14
owner_team = checkout
# Playbook accessing Custom Local Facts
- name: Display custom datacenter location fact
  ansible.builtin.debug:
    msg: "Host is in Datacenter {{ ansible_facts.ansible_local.datacenter.location.datacenter_code }}, Rack {{ ansible_facts.ansible_local.datacenter.location.rack_id }}"

Topic 3: The 22-Level Variable Precedence Hierarchy

When the same variable name (e.g., http_port) is defined in multiple locations, Ansible evaluates the final value according to a strict 22-level precedence hierarchy. Higher levels override lower levels:

 ┌─────────────────────────────────────────────────────────────┐
 │ 22 (Highest): Extra Vars (-e "var=value")                   │ ← HIGHEST: ALWAYS WINS!
 ├─────────────────────────────────────────────────────────────┤
 │ 21: Task Block vars                                         │
 ├─────────────────────────────────────────────────────────────┤
 │ 20: Task vars (only for specific task)                      │
 ├─────────────────────────────────────────────────────────────┤
 │ 19: Role vars (roles/x/vars/main.yml)                       │
 ├─────────────────────────────────────────────────────────────┤
 │ 16: Play vars (vars: inside play)                           │
 ├─────────────────────────────────────────────────────────────┤
 │ 13: Host Facts / ansible_facts                              │
 ├─────────────────────────────────────────────────────────────┤
 │ 10: host_vars files (host_vars/web1.yml)                    │
 ├─────────────────────────────────────────────────────────────┤
 │ 6: group_vars files (group_vars/webservers.yml)             │
 ├─────────────────────────────────────────────────────────────┤
 │ 3: group_vars/all.yml                                       │
 ├─────────────────────────────────────────────────────────────┤
 │ 1 (Lowest): Role Defaults (roles/x/defaults/main.yml)       │ ← LOWEST: EASILY OVERRIDDEN
 └─────────────────────────────────────────────────────────────┘

Production Golden Rules of Variable Placement:

  1. Use Role Defaults (roles/x/defaults/main.yml) for default variable settings inside roles that users are EXPECTED to override in group_vars or host_vars.
  2. Use Role Vars (roles/x/vars/main.yml) ONLY for internal role constants that users SHOULD NEVER override.
  3. Use group_vars/all.yml for organization-wide defaults (NTP servers, domain names, syslog endpoints).
  4. Use group_vars/<group_name>.yml for environment or tier defaults (webservers.yml, dbservers.yml).
  5. Use Extra Vars (ansible-playbook -e "var=val") ONLY for temporary CLI overrides or CI/CD build number injections.

Topic 4: Variable Naming Standards & Preventing Namespace Pollution

Avoid generic variable names like port: 80, user: admin, or version: 1.0.

If two different roles define port: 80 and port: 5432, the last evaluated role will silently overwrite the variable for the entire play!

Red Hat Standard: Prefix Role Variables: Always prefix variable names with the role name:

  • Role nginx: nginx_http_port: 80 nginx_worker_processes: 4 nginx_config_path: /etc/nginx/nginx.conf

  • Role postgresql: postgresql_port: 5432 postgresql_max_connections: 200 postgresql_data_dir: /var/lib/postgresql/data

# Verifying variable resolution via debug module
- name: Debug resolved role variable
  ansible.builtin.debug:
    var: nginx_http_port

Common mistake: Leaving fact gathering on for plays that never read a fact. It adds a full round trip per host to every run — and the opposite mistake is worse: setting gather_facts: false on a play whose template quietly uses ansible_default_ipv4, which then renders empty rather than failing.