Lab 08: Ansible Variables, Templates & Handlers

Time: 45 minutes | Level: Architect | Docker: docker run -it --rm ubuntu:22.04 bash

Overview

Variables are the lifeblood of reusable Ansible automation. Ansible has 16 levels of variable precedence, Jinja2 templating for dynamic configuration files, and handlers for event-driven service management. This lab covers variable precedence, register + debug, Jinja2 templates with loops/conditionals, handlers with notify/listen, include_tasks/import_tasks, and when conditions.

Prerequisites

  • Completed Lab 06 (Ansible Foundations)

  • Basic understanding of Jinja2 templating syntax


Step 1: Variable Precedence — 16 Levels

docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq 2>/dev/null && apt-get install -y -qq python3-pip python3 2>/dev/null
pip3 install ansible --quiet 2>/dev/null

cat > /tmp/hosts.ini << 'EOF'
[webservers]
localhost ansible_connection=local http_port=9090

[webservers:vars]
http_port=8080
app_name=myapp
EOF

mkdir -p /tmp/group_vars /tmp/host_vars

cat > /tmp/group_vars/webservers.yml << 'EOF'
---
http_port: 7070
max_connections: 500
EOF

cat > /tmp/host_vars/localhost.yml << 'EOF'
---
http_port: 6060
server_role: primary
EOF

cat > /tmp/precedence_test.yml << 'EOF'
---
- name: Variable precedence demonstration
  hosts: webservers
  gather_facts: false
  vars:
    http_port: 5050
    play_var: from_play

  tasks:
    - name: Show http_port (play vars override group/host vars)
      debug:
        msg: \"http_port = {{ http_port }} (play vars win over group_vars/host_vars)\"

    - name: Extra vars override everything
      debug:
        msg: \"extra_var = {{ extra_var | default('not set - use -e to set') }}\"

    - name: Show all variable sources
      debug:
        msg: |
          Variable Precedence (lowest to highest):
          1.  role defaults (defaults/main.yml)
          2.  inventory file or script group vars
          3.  inventory group_vars/all
          4.  playbook group_vars/all
          5.  inventory group_vars/*
          6.  playbook group_vars/*
          7.  inventory file or script host vars
          8.  inventory host_vars/*
          9.  playbook host_vars/*
          10. host facts / cached set_facts
          11. play vars
          12. play vars_prompt
          13. play vars_files
          14. role vars (vars/main.yml)
          15. block vars (only for tasks in block)
          16. task vars (only for the task)
          17. include_vars
          18. set_facts / registered vars
          19. role (and include_role) params
          20. include params
          21. extra vars (-e) ALWAYS WIN
EOF

ansible-playbook -i /tmp/hosts.ini /tmp/precedence_test.yml 2>&1
echo ''
echo '--- With extra vars (-e overrides everything) ---'
ansible-playbook -i /tmp/hosts.ini /tmp/precedence_test.yml -e 'extra_var=from_command_line http_port=1111' 2>&1 | grep 'http_port\|extra_var'
"

📸 Verified Output:

💡 Tip: The golden rule: extra vars (-e) always win. Use them for environment-specific overrides in CI/CD: ansible-playbook deploy.yml -e "env=prod version=2.1.0". Use defaults/ for documentation-friendly defaults that users should override.


Step 2: vars, vars_files, and register

📸 Verified Output:

💡 Tip: register captures the entire module result as a dict. Common keys: stdout, stderr, rc (return code), stdout_lines (list), changed, failed. Always check register_var.rc == 0 before using results in critical tasks.


Step 3: Jinja2 Templates — .j2 Files

📸 Verified Output:

💡 Tip: Always add # Managed by Ansible — DO NOT EDIT MANUALLY at the top of templates. Use {{ variable | default('fallback') }} to avoid undefined errors. The template module also sets changed correctly — it only overwrites if content differs, triggering handlers only when needed.


Step 4: Handlers — notify and listen

📸 Verified Output:

💡 Tip: Handlers run ONCE at the end of a play, even if notified multiple times. Use listen to have multiple tasks trigger the same logical event. Use meta: flush_handlers when you need handlers to run mid-play (e.g., reload nginx before deploying content to it).


Step 5: when Conditions and Loops

📸 Verified Output:

💡 Tip: Use loop_control: label: to control what Ansible displays per iteration (avoids dumping the full dict). For simple lists, loop is preferred over with_items. When combining when + loop, the condition is evaluated per item — perfect for filtering.


Step 6: include_tasks and import_tasks

📸 Verified Output:

💡 Tip: Rule of thumb: use import_tasks for static includes that should show up in --list-tasks and --check mode. Use include_tasks when you need runtime dynamism (variables, loops, conditions). Never use include (deprecated) — always specify include_tasks or import_tasks.


Step 7: Advanced Jinja2 Filters and Tests

📸 Verified Output:

💡 Tip: The | default() filter is your best friend — always use it for optional variables to prevent "undefined variable" errors. Chain filters with pipes: {{ value | default('') | upper | trim }}. Use | bool to safely convert strings like "true"/"yes"/"1" to Python booleans.


Step 8: Capstone — Dynamic Configuration Generator

Scenario: Your infrastructure team needs to generate configuration files for 3 different environments (dev/staging/prod) with different settings, using a single template and variable files per environment.

📸 Verified Output:

💡 Tip: This pattern — single template + per-environment var files — is the foundation of infrastructure-as-code. The template becomes your documentation; the var files become your environment specification. Combine this with Ansible Vault (Lab 09) to encrypt sensitive prod values.


Summary

Concept
Syntax
Purpose

Var precedence

role defaults < group_vars < host_vars < play vars < extra vars

Know what wins

Extra vars

ansible-playbook -e "key=val"

Always highest priority

vars_files

vars_files: [file.yml]

Load vars from external file

register

register: result_var

Capture task output

debug output

debug: var: result.stdout

Print registered var

Jinja2 variable

{{ variable_name }}

Insert value

Jinja2 filter

{{ var | upper | default('x') }}

Transform value

Jinja2 condition

{% if condition %} ... {% endif %}

Conditional block

Jinja2 loop

{% for item in list %} ... {% endfor %}

Loop in template

template module

template: src: x.j2 dest: /path

Render + deploy template

Handler

handlers: - name: X + notify: X

Event-driven triggers

Listen

listen: event_name

Multiple handlers, one event

flush_handlers

meta: flush_handlers

Run handlers mid-play

when condition

when: var == 'value'

Conditional task execution

loop

loop: "{{ list }}"

Iterate over list/dict

loop_control

loop_control: label: "{{ item.name }}"

Control loop display

import_tasks

import_tasks: file.yml

Static task inclusion

include_tasks

include_tasks: file.yml

Dynamic task inclusion

Last updated