A template fills the gaps, a handler waits
An Ansible template is a text file with gaps that Ansible fills from variables, using a language called Jinja2. A handler is a task that runs only when another task changed something, and only once, however many tasks asked for it.
Together they let a role rewrite a settings file and restart a service only when the file really changed.
By the end of this lesson
Note
I can fill a template from variables and run a handler once.
Key words
| Word | What it means | Example |
|---|---|---|
| Template | a text file with gaps filled from variables | board.txt.j2 |
| Jinja2 | the language of the gaps, loops and conditions | |
| Loop | a for block that repeats lines for each item | |
| Condition | an if block that keeps or drops lines | |
| Handler | a task that runs only when it is told to | |
| Notify | a task's request for a handler to run |
The cells students fill in are left blank in this preview.
A template is a file with gaps
Double braces mark a gap: {{ shop_name }} is replaced by the variable's value. A line of the form {% for item in specials %} starts a loop, and {% endfor %} ends it.
A line of the form {% if late_opening %} keeps what follows only when the condition is true. The lines with {% in them make no output of their own.
A notice board template
# board.txt.j2
Notice board for {{ shop_name }}
{% for item in specials %}
- {{ item }}
{% endfor %}
{% if late_opening %}
Open late on Fridays
{% endif %}
Worked example: what the template makes
| specials | late_opening | Lines the file gets |
|---|---|---|
| rye loaf, lemon tart | false | Notice board for Corner Shop, - rye loaf, - lemon tart |
| rye loaf, lemon tart | true | The same three lines, then Open late on Fridays |
| none | false | Notice board for Corner Shop |
The cells students fill in are left blank in this preview.
Make the Ansible work folder
mkdir -p ~/iac-demo/ansibleShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Go into it
cd ~/iac-demo/ansibleShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Build the layout of a role called board
ansible-galaxy role init --init-path roles boardShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write its defaults
printf '%s\n' '---' 'shop_name: Corner Shop' 'late_opening: false' 'specials:' ' - rye loaf' ' - lemon tart' > roles/board/defaults/main.ymlShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the template shown above
printf '%s\n' 'Notice board for {{ shop_name }}' '{% for item in specials %}' '- {{ item }}' '{% endfor %}' '{% if late_opening %}' 'Open late on Fridays' '{% endif %}' > roles/board/templates/board.txt.j2Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write two tasks that both notify a handler
printf '%s\n' '---' '- name: Make the folder' ' ansible.builtin.file:' ' path: /tmp/iac-board' ' state: directory' ' mode: "0755"' ' notify: Tell the staff' '' '- name: Write the board' ' ansible.builtin.template:' ' src: board.txt.j2' ' dest: /tmp/iac-board/board.txt' ' mode: "0644"' ' notify: Tell the staff' > roles/board/tasks/main.ymlShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the handler
printf '%s\n' '---' '- name: Tell the staff' ' ansible.builtin.debug:' ' msg: The board has changed' > roles/board/handlers/main.ymlShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write a play that uses the role
printf '%s\n' '---' '- name: Keep the notice board' ' hosts: localhost' ' gather_facts: false' ' roles:' ' - board' > board.ymlShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A handler runs once, at the end
A task asks for a handler with notify. Ansible does not run the handler at once. It keeps a list of the handlers that were asked for, and runs each one once when the play ends.
A task that changed nothing asks for nothing. A task called meta: flush_handlers runs the waiting handlers early, for a role whose later tasks need the handler's work done first.
First run: both tasks change something
ansible-playbook -i localhost, -c local board.yml | grep -oE "RUNNING HANDLER|changed=[0-9]+"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Read the file the template made
cat /tmp/iac-board/board.txtShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Second run: nothing is different
ansible-playbook -i localhost, -c local board.yml | grep -oE "RUNNING HANDLER|changed=[0-9]+"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A common mistake
Warning
Many people think each notify runs the handler again, so three tasks mean three runs. Ansible collects the requests. At the end of the play it runs the handler one time, and only if a task really changed something.
Turn late opening on, using JSON
ansible-playbook -i localhost, -c local board.yml -e '{"late_opening": true}' | grep -oE "RUNNING HANDLER|changed=[0-9]+"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Read the file again
cat /tmp/iac-board/board.txtShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Text is not a Boolean
Important
A value typed as -e late_opening=false is the text false, and any text counts as true in a condition. To pass a real false, give JSON: -e '{"late_opening": false}'.
Dry run: show the line that would go
ansible-playbook -i localhost, -c local board.yml -e '{"late_opening": false}' --check --diff | grep -E "^-[^-]|^\+[^+]"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A template and a handler in Python
# What the template makes, and when the handler runs.
specials = ["rye loaf", "lemon tart"]
late_opening = True
lines = ["Notice board for Corner Shop"]
for item in specials:
lines.append("- " + item)
if late_opening:
lines.append("Open late on Fridays")
print("\n".join(lines))
# A handler waits. It runs at a flush, and once more at the end if asked again.
steps = ["changed", "flush", "changed", "ok"]
waiting = False
runs = 0
for step in steps:
if step == "changed":
waiting = True
elif step == "flush" and waiting:
runs = runs + 1
waiting = False
if waiting:
runs = runs + 1
print("handler runs:", runs)
# Remove the flush from steps and run it again.