Learning objectives
By the end of this module, you should be able to:
- Use loop for repeated structured items.
- Render a Jinja template using variables.
- Validate templated configuration before broad rollout.
Loops remove duplicated task definitions
When the action is identical and only the data changes, a loop can apply the same module to multiple items. This is clearer than copying the task several times and reduces the chance of inconsistent edits.
Templates turn data into configuration
The template module renders a Jinja template on the control side using variables and facts, then manages the rendered file on the target. Templates are useful for configuration files that vary by host, group or environment.
Treat templating as code
Current ansible-core releases have tightened templating behaviour, so legacy content that relied on ambiguous or unsafe template expressions may require changes. Keep expressions simple, validate upgraded automation, and consult the applicable porting guide when moving between major ansible-core versions.
Worked examples
See the idea in practice.
Create multiple users with a loop
- name: Ensure training users exist
ansible.builtin.user:
name: '{{ item.name }}'
groups: '{{ item.groups }}'
state: present
loop:
- { name: 'alice', groups: 'wheel' }
- { name: 'bob', groups: 'developers' }
- The action stays the same while each item supplies data.
- Structured loop items are clearer than parsing strings.
Template file
# app.conf.j2
server_name={{ inventory_hostname }}
listen_port={{ app_port }}
environment={{ environment_name }}
- Template expressions are replaced with current variable values.
- One template can produce different configuration for different hosts.
Deploy the template
- name: Render application configuration
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
owner: root
group: root
mode: '0644'
- The module manages destination content and attributes.
- It can participate in check and diff workflows when supported.
Hands-on lab
Lab — Generate host-specific configuration
Goal: Render a different configuration file for each managed host using the same template.
STEP 1Create app.conf.j2
server_name={{ inventory_hostname }}
listen_port={{ app_port }}
environment={{ environment_name }}
STEP 2Define variables
vars:
app_port: 8080
environment_name: training
STEP 3Deploy with template
- name: Render training configuration
ansible.builtin.template:
src: app.conf.j2
dest: /tmp/app.conf
mode: '0644'
STEP 4Preview differences
ansible-playbook -i inventory.ini template.yml --check --diff --limit web01
STEP 5Apply and inspect
ansible-playbook -i inventory.ini template.yml --limit web01
ansible web01 -i inventory.ini -m ansible.builtin.command -a 'cat /tmp/app.conf'
Verify
- The rendered server_name matches the target inventory hostname.
- The selected port and environment appear correctly.
- A clean rerun does not rewrite identical content.
Expected outcome
- One source template produces correct target-specific configuration.
If it fails
- If a variable is undefined, inspect variable scope and naming before adding defaults blindly.
- Be careful with --diff on sensitive files because diffs may expose secret content.
Real-world connectionTemplates are common for service configuration, but sensitive values should be protected and diff output disabled where exposure is possible.
Avoid these traps
Common mistakes
- Copy-pasting nearly identical tasks instead of looping over structured data.
- Putting complex business logic into templates that would be clearer in data or tasks.
- Using --diff on files that contain secrets.
Knowledge check
Can you explain it without looking back?
Open each answer only after you have tried to answer the question yourself.
1When is a loop appropriate?
When the same action should run for multiple data items.
2Where is a Jinja template rendered for a normal template task?
Ansible renders it using control-side execution context and then manages the resulting file on the target.
3Why validate templating during ansible-core upgrades?
Templating behaviour can change between core releases, and newer versions may reject ambiguous legacy patterns.