Ansible Essentials: Variables, Facts & Templates

Instead of hardcoding values in every task, you define variables once and reference them everywhere. Facts go further — Ansible automatically discovers information about each host that you can use without defining anything manually. Once you have variables and facts to work with, Jinja2 templates let you project them into config files — instead of maintaining ten nearly identical files by hand, you write a single file with placeholders that Ansible fills in per host at deploy time.

Variable Sources

SourceScopeExample use
group_vars/all.ymlAll hostsNTP server, DNS
group_vars/<group>.ymlHosts in a groupSubnet per VLAN
host_vars/<host>.ymlSingle hostPer-host port
Playbook vars:Current playOne-off overrides

host_vars overrides group_vars, which overrides all. Playbook vars: overrides all of these.

Defining Variables

BASH
homelab-ansible/
├── group_vars/
│   ├── all.yml
│   └── servers.yml
└── host_vars/
    └── web01.yml
group_vars/all.yml
ntp_server: "pool.ntp.org"
timezone: "Europe/Amsterdam"
host_vars/web01.yml
http_port: 8080

Use them in tasks with {{ variable_name }}:

tasks/main.yml
- name: Set timezone
  community.general.timezone:
    name: "{{ timezone }}"

Ansible Facts

When gather_facts: true (the default), Ansible collects system information from the host at the start of each play.

FactExample value
ansible_hostnameweb01
ansible_default_ipv4.address192.168.1.10
ansible_distributionUbuntu
ansible_os_familyDebian
ansible_memtotal_mb7976

To see all available facts for a host:

BASH
ansible web01 -m setup

Using Facts in Tasks

tasks/main.yml
- name: Install packages (Debian)
  ansible.builtin.apt:
    name: curl
    state: present
  when: ansible_os_family == "Debian"

- name: Install packages (RedHat)
  ansible.builtin.dnf:
    name: curl
    state: present
  when: ansible_os_family == "RedHat"

Registering Task Output

Use register to capture a task result and act on it:

tasks/main.yml
- name: Check if nginx is running
  ansible.builtin.systemd:
    name: nginx
  register: nginx_status
  changed_when: false
  failed_when: false

- name: Start nginx if not running
  ansible.builtin.systemd:
    name: nginx
    state: started
  when: nginx_status.status is defined and nginx_status.status.ActiveState != "active"

The debug Module

Use debug to inspect variables while developing:

YAML
- name: Show host IP
  ansible.builtin.debug:
    msg: "Host IP is {{ ansible_default_ipv4.address }}"

- name: Dump all facts
  ansible.builtin.debug:
    var: ansible_facts

Now that you have variables and facts to work with, let’s put them to use: projecting them into real configuration files with Jinja2 templates.

Template Syntax

Templates are plain text files ending in .j2 with three main expressions:

SyntaxPurpose
{{ variable }}Insert a variable value
{% if condition %}Conditional block
{% for item in list %}Loop

Where Templates Live

Place templates inside your role under a templates/ directory:

BASH
roles/
└── monitoring/
    ├── tasks/
    │   └── main.yml
    └── templates/
        └── prometheus-targets.j2

The template Module

Use template instead of copy whenever you need variable substitution:

tasks/main.yml
- name: Deploy Prometheus scrape config
  ansible.builtin.template:
    src: prometheus-targets.j2
    dest: /etc/prometheus/targets.yml
    owner: prometheus
    group: prometheus
    mode: '0644'
  notify: Reload Prometheus

Writing a Template

This template generates a Prometheus scrape config from your inventory:

prometheus-targets.j2
# Managed by Ansible — do not edit manually
scrape_configs:
  - job_name: node_exporter
    static_configs:
      - targets:
{%- for host in groups['homelab'] %}
          - "{{ hostvars[host]['ansible_host'] }}:9100"
{%- endfor %}

Add a host to your inventory and the next run updates the config automatically.

Use {% if %} to include sections conditionally:

JINJA2
{% if enable_https %}
    scheme: https
    tls_config:
      insecure_skip_verify: true
{% endif %}

Create the Playbook

monitoring.yml
- name: Deploy monitoring config
  hosts: monitoring_server
  become: true
  roles:
    - monitoring

Run It

BASH
ansible-playbook playbooks/monitoring.yml --ask-become-pass

Recap

You’ve now covered:

  • Defining variables in group_vars and host_vars
  • Using gathered facts like ansible_os_family for host-aware tasks
  • Capturing task output with register
  • Inspecting variables with debug
  • Writing .j2 templates using {{ }}, {% if %}, and {% for %} syntax
  • Placing templates inside a role’s templates/ directory
  • Deploying rendered files with the template module

Next up: Managing Secrets with Vault — how to keep passwords and API keys out of version control while still using them in your playbooks.

License

Author: Sven van Ginkel

Link: https://svenvg.com/posts/ansible-essentials-variables-facts-templates/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.