Variables and Templates
Variables and Jinja2 templates make Ansible playbooks dynamic. Variables hold data; templates use that data to generate configuration files.
Defining Variables
Section titled “Defining Variables”In a Playbook
Section titled “In a Playbook”- 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.ymlIn Inventory (group_vars / host_vars)
Section titled “In Inventory (group_vars / host_vars)”app_port: 8080app_env: production
# host_vars/web1.example.com.ymlapp_port: 9090 # override for this specific hostIn Role Defaults
Section titled “In Role Defaults”app_port: 8080app_env: developmentapp_log_level: infoOn the Command Line
Section titled “On the Command Line”ansible-playbook site.yml -e "app_env=staging app_port=3000"ansible-playbook site.yml -e @vars/staging.yml # from a fileVariable Precedence
Section titled “Variable Precedence”Ansible has 22 levels of precedence. The most important ones (lowest to highest):
| Priority | Source |
|---|---|
| 1 (lowest) | Role defaults (defaults/main.yml) |
| 2 | Inventory group vars |
| 3 | Inventory host vars |
| 4 | Playbook group_vars/ |
| 5 | Playbook host_vars/ |
| 6 | Playbook vars: |
| 7 | Role vars (vars/main.yml) |
| 8 | Task vars: |
| 9 | set_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 }}"Common Facts
Section titled “Common Facts”| Fact | Example Value |
|---|---|
ansible_distribution | Ubuntu, CentOS, Debian |
ansible_distribution_version | 22.04, 8.5 |
ansible_os_family | Debian, RedHat |
ansible_hostname | web1 |
ansible_fqdn | web1.example.com |
ansible_default_ipv4.address | 10.0.1.5 |
ansible_memtotal_mb | 8192 |
ansible_processor_vcpus | 4 |
Viewing All Facts
Section titled “Viewing All Facts”ansible web1.example.com -m setup # all factsansible web1.example.com -m setup -a "filter=ansible_distribution*" # filteredDisabling Fact Gathering
Section titled “Disabling Fact Gathering”If you don’t need facts (speeds up execution):
- name: Quick task hosts: webservers gather_facts: false tasks: - name: Ping ping:Custom Facts
Section titled “Custom Facts”Place a script or JSON/INI file in /etc/ansible/facts.d/ on the managed host:
[general]app_version=2.1.0environment=productionAccess with ansible_local.app.general.app_version.
set_fact
Section titled “set_fact”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 }}"Jinja2 Templates
Section titled “Jinja2 Templates”Templates are text files with Jinja2 expressions that get rendered with Ansible variables. They live in templates/ and typically end in .j2.
Basic Substitution
Section titled “Basic Substitution”app: port: {{ app_port }} environment: {{ app_env }} database: host: {{ db_host }} port: {{ db_port | default(5432) }} name: {{ db_name }}Conditionals
Section titled “Conditionals”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 }}; }}{% for host in app_servers %}{{ hostvars[host].ansible_default_ipv4.address }} {{ host }}{% endfor %}{% for key, value in env_vars.items() %}{{ key }}={{ value }}{% endfor %}Comments
Section titled “Comments”{# This is a Jinja2 comment — not included in output #}Filters
Section titled “Filters”Filters transform values using |:
String Filters
Section titled “String Filters”"{{ 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') }}"Default Values
Section titled “Default Values”"{{ 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 defaultList/Dict Filters
Section titled “List/Dict Filters”"{{ 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 listsType Conversion
Section titled “Type Conversion”"{{ '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 dictHashing and Encoding
Section titled “Hashing and Encoding”"{{ 'password' | password_hash('sha512') }}" # hashed password for user module"{{ content | b64encode }}" # base64 encode"{{ encoded | b64decode }}" # base64 decodeLookups
Section titled “Lookups”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.
Key Takeaways
Section titled “Key Takeaways”- Extra vars (
-e) always have highest precedence; roledefaults/have the lowest. - Facts provide system info automatically — OS, IPs, memory, CPU.
- Use
set_factto 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/andhost_vars/files over inline variables for maintainability.