Skip to content

Variables and Templates

First PublishedByAtif Alam

Variables and Jinja2 templates make Ansible playbooks dynamic. Variables hold data; templates use that data to generate configuration files.

- name: Configure app
hosts: webservers
vars:
app_port: 8080
app_env: production
db_host: db1.example.com
tasks:
- name: Deploy config
template:
src: app.conf.j2
dest: /etc/app/config.yml
group_vars/webservers.yml
app_port: 8080
app_env: production
# host_vars/web1.example.com.yml
app_port: 9090 # override for this specific host
roles/myapp/defaults/main.yml
app_port: 8080
app_env: development
app_log_level: info
Terminal window
ansible-playbook site.yml -e "app_env=staging app_port=3000"
ansible-playbook site.yml -e @vars/staging.yml # from a file

Ansible has 22 levels of precedence. The most important ones (lowest to highest):

PrioritySource
1 (lowest)Role defaults (defaults/main.yml)
2Inventory group vars
3Inventory host vars
4Playbook group_vars/
5Playbook host_vars/
6Playbook vars:
7Role vars (vars/main.yml)
8Task vars:
9set_fact / register
10 (highest)Extra vars (-e on command line)

Rule of thumb: Extra vars (-e) always win. Role defaults are always the easiest to override. Put variables users should change in defaults/; put internal constants in vars/.

Ansible automatically gathers facts about each host — OS, IP addresses, memory, disk, etc.

tasks:
- name: Show OS info
debug:
msg: "{{ ansible_distribution }} {{ ansible_distribution_version }} ({{ ansible_os_family }})"
# Output: "Ubuntu 22.04 (Debian)"
- name: Show IP
debug:
msg: "{{ ansible_default_ipv4.address }}"
FactExample Value
ansible_distributionUbuntu, CentOS, Debian
ansible_distribution_version22.04, 8.5
ansible_os_familyDebian, RedHat
ansible_hostnameweb1
ansible_fqdnweb1.example.com
ansible_default_ipv4.address10.0.1.5
ansible_memtotal_mb8192
ansible_processor_vcpus4
Terminal window
ansible web1.example.com -m setup # all facts
ansible web1.example.com -m setup -a "filter=ansible_distribution*" # filtered

If you don’t need facts (speeds up execution):

- name: Quick task
hosts: webservers
gather_facts: false
tasks:
- name: Ping
ping:

Place a script or JSON/INI file in /etc/ansible/facts.d/ on the managed host:

/etc/ansible/facts.d/app.fact
[general]
app_version=2.1.0
environment=production

Access with ansible_local.app.general.app_version.

Define variables dynamically during a play:

tasks:
- name: Get app version
command: /opt/myapp/bin/myapp --version
register: version_output
changed_when: false
- name: Set version fact
set_fact:
app_version: "{{ version_output.stdout | trim }}"
- name: Use the fact
debug:
msg: "Running version {{ app_version }}"

Templates are text files with Jinja2 expressions that get rendered with Ansible variables. They live in templates/ and typically end in .j2.

templates/app.conf.j2
app:
port: {{ app_port }}
environment: {{ app_env }}
database:
host: {{ db_host }}
port: {{ db_port | default(5432) }}
name: {{ db_name }}
templates/nginx.conf.j2
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
location / {
proxy_pass http://127.0.0.1:{{ app_port }};
}
}
templates/hosts.j2
{% for host in app_servers %}
{{ hostvars[host].ansible_default_ipv4.address }} {{ host }}
{% endfor %}
templates/env.j2
{% for key, value in env_vars.items() %}
{{ key }}={{ value }}
{% endfor %}
{# This is a Jinja2 comment — not included in output #}

Filters transform values using |:

"{{ hostname | upper }}" # WEB1
"{{ hostname | lower }}" # web1
"{{ hostname | capitalize }}" # Web1
"{{ path | basename }}" # file.txt (from /etc/app/file.txt)
"{{ path | dirname }}" # /etc/app
"{{ name | regex_replace('old', 'new') }}"
"{{ db_port | default(5432) }}" # use 5432 if db_port is undefined
"{{ optional_var | default(omit) }}" # omit the parameter entirely if undefined
"{{ items | default([]) }}" # empty list as default
"{{ my_list | join(', ') }}" # "a, b, c"
"{{ my_list | unique }}" # deduplicate
"{{ my_list | sort }}" # sort
"{{ my_list | length }}" # count
"{{ my_list | first }}" # first element
"{{ my_list | last }}" # last element
"{{ my_dict | dict2items }}" # convert dict to list of {key, value}
"{{ my_list | items2dict }}" # reverse
"{{ [list1, list2] | flatten }}" # merge nested lists
"{{ '8080' | int }}" # 8080 (integer)
"{{ 8080 | string }}" # "8080" (string)
"{{ 'true' | bool }}" # true (boolean)
"{{ my_dict | to_json }}" # JSON string
"{{ my_dict | to_yaml }}" # YAML string
"{{ json_string | from_json }}" # parse JSON to dict
"{{ 'password' | password_hash('sha512') }}" # hashed password for user module
"{{ content | b64encode }}" # base64 encode
"{{ encoded | b64decode }}" # base64 decode

Lookups pull data from external sources on the control node:

# Read a file
"{{ lookup('file', '/path/to/local/file.txt') }}"
# Read an environment variable
"{{ lookup('env', 'HOME') }}"
# Read from a password store
"{{ lookup('password', '/tmp/passwordfile length=20') }}"
# Read lines from a file
"{{ lookup('lines', 'cat /etc/hosts') }}"
# Read a CSV file
"{{ lookup('csvfile', 'key1 file=data.csv delimiter=,') }}"

Lookups run on the control node, not on managed hosts. For remote data, use register with a task.

  • Extra vars (-e) always have highest precedence; role defaults/ have the lowest.
  • Facts provide system info automatically — OS, IPs, memory, CPU.
  • Use set_fact to create variables dynamically from task output.
  • Jinja2 templates support {{ variables }}, {% if %}, {% for %}, and filters.
  • Use | default(value) liberally to handle undefined variables gracefully.
  • Lookups pull data from the control node (files, env vars, passwords).
  • Prefer group_vars/ and host_vars/ files over inline variables for maintainability.