Courseiva
EX294Chapter 8 of 18Objective 3.5

Templates Using Jinja2

For the Red Hat Certified Engineer EX294 exam, you must master automation with Ansible, and a core part of that is dynamically generating configuration files. This chapter solves the problem of manually editing hundreds of server configuration files by teaching you how to use Jinja2 templates to create them automatically. Understanding Jinja2 is not optional; it is a tested objective you must pass.

12 min read
Intermediate
Updated Jul 23, 2026
Editorial oversight: Johnson Ajibi· Security Systems Architect · MSc IT Security · IEEE Senior Member

A simple way to picture Templates Using Jinja2

The Custom Personalised Letter Analogy

Have you ever needed to send the same basic letter to dozens of different people but wanted each one to look like it was written just for them?

Think of writing a 'template' letter on your computer that has blanks for the person's name, their address, and maybe a specific offer based on what they like. You create one document with placeholders like {{name}} and {{offer}}. Then, you have a spreadsheet with each person's details: name, address, and the offer code. You use a mail-merge feature, and your word processor creates a unique, personalised letter for every single person by filling in the blanks from the spreadsheet.

Jinja2 templates work exactly like that mail-merge, but for IT configuration files. Instead of a letter, you have a configuration file for a piece of software. Instead of a spreadsheet, you have data stored in a special file (like a variable file). When you run Ansible, it acts like the mail processor: it takes your template, finds the placeholder variables like {{server_name}} or {{port_number}}, and fills them with the real data from your variable file. This turns one generic template into hundreds of perfectly customised configuration files, saving you from manually editing every single config file by hand.

How It Actually Works

Jinja2 is a templating engine, which is a fancy term for a tool that lets you create a file with placeholders and then automatically fill in those placeholders with real data. In the context of Ansible, it is a crucial way to manage configuration files for different servers. Instead of having a separate config file for every single server, you create one 'template' file and let Ansible do the heavy lifting.

Before templates, system administrators would manually edit configuration files using text editors, like VI. If you had 100 web servers, and you needed to change the port number on every single one, you would have to log into each server, open the file, find the line with the port number, change it, save the file, and restart the service. This is incredibly tedious, error-prone, and slow. Jinja2 completely replaces this manual process.

The way it works is simple. You create a file with a '.j2' extension, which stands for Jinja2. Inside this file, you write the configuration you want, but instead of hard-coding values (like a specific IP address), you use variables. Variables are placeholders that look like this: {{ variable_name }}. When Ansible processes this template, it replaces {{ variable_name }} with the actual value that is stored in your Ansible variables.

For example, a line in a template might look like: 'Listen {{ http_port }}'. If the variable 'http_port' is defined as 8080, the generated file will contain the line 'Listen 8080'. If 'http_port' is 80, it becomes 'Listen 80'. This single template can generate a unique config for every server.

Jinja2 also supports much more than simple variable replacement. It includes powerful features like:

Logic statements: You can use 'if' statements to conditionally include parts of the config. For example, '{% if server_role == "database" %}' allows you to include a specific database configuration block only for servers whose role is 'database'.

Loops: You can use 'for' loops to repeat a section of the config. For example, '{% for user in users %}' will generate a configuration block for each user in a list of users.

Filters: These modify the value of a variable. For example, '{{ some_variable | upper }}' will output the value of some_variable in all uppercase letters. '{{ my_list | join(", ") }}' will join all items in a list into a single string separated by commas.

Arithmetic: You can perform calculations inside the template, like {{ total_memory / 2 }} to calculate half of a total memory value.

Ansible uses the 'template' module to apply the Jinja2 template to a managed node. The task syntax looks like this:

- name: Generate config file ansible.builtin.template: src: my_config.j2 dest: /etc/myapp/config.conf owner: root group: root mode: '0644'

This task tells Ansible to take the template file 'my_config.j2' from the control node, process it using the variables available to that specific host, and deploy the resulting file to the destination '/etc/myapp/config.conf' on the managed node. It can also set file permissions and ownership.

Why does this exist? It solves the problem of configuration drift. When you manually edit files, you inevitably end up with servers that are slightly different from each other (drift). Templates guarantee every server using the same role gets the same configuration, with only the bits that need to be different (like IP addresses) being customised. This is the heart of 'Infrastructure as Code' — defining your infrastructure in version-controlled files rather than through manual, fragile processes.

Flow diagram showing how one template file and host-specific variables create unique configuration files for each server.

Walk-Through

1

Plan your template

Identify the configuration file you want to generate. Look for values that change between servers, like IP addresses, hostnames, ports, or usernames. These values will become your Jinja2 variables.

2

Create the template file

Create a new file with a '.j2' extension, e.g., 'app_config.j2'. Copy the original configuration file into it. Replace the hard-coded values with Jinja2 variables, e.g., change 'port=80' to 'port={{ app_port }}'. Add any 'for' loops or 'if' statements as needed.

3

Define the variables

Ensure the variables you used in the template are defined in your Ansible inventory. You can define them in host_vars, group_vars, or in the playbook itself. For example, set 'app_port: 8080' in your group_vars for web servers.

4

Write the playbook task

In your playbook, add a task that uses the 'ansible.builtin.template' module. Set the 'src' parameter to the path of your template file (e.g., 'app_config.j2') and the 'dest' parameter to the full path where the file should be placed on the target server (e.g., '/etc/myapp/config.conf'). Optionally set 'owner', 'group', and 'mode'.

5

Run the playbook

Execute your playbook using the 'ansible-playbook' command. Ansible will: 1) gather facts from the target host, 2) replace all template variables with host-specific values, 3) copy the rendered file to the target destination, and 4) report success or any errors.

6

Verify the output

Check the generated file on the managed host to ensure the variables were substituted correctly. For example, use 'cat /etc/myapp/config.conf' to see the final rendered configuration. Debug any errors by checking the syntax of your template, especially matching 'for' and 'if' blocks.

What This Looks Like on the Job

Consider a real-world company, 'FastShop', an online retailer. They run hundreds of web servers behind a load balancer. Each web server hosts the same application but needs to know its own unique IP address and hostname to register with the central monitoring system. The configuration file for the monitoring agent is almost identical on every server, except for these two details.

Before Ansible, a junior admin would have to manually update each config file whenever a new server was added or an IP changed. This often led to mistakes: a typo in an IP address, a forgotten hostname, or a server being added without the correct monitoring configuration.

With Jinja2 templates, the company creates one single template file called 'monitoring_agent.conf.j2'. This template looks something like this:

[agent] server_hostname={{ inventory_hostname }} server_ip={{ ansible_default_ipv4.address }} monitoring_server=monitor.fastshop.com agent_port=9876

The {{ inventory_hostname }} is a special Ansible variable that automatically contains the hostname of the server Ansible is currently configuring. The {{ ansible_default_ipv4.address }} is another special variable (called a 'fact') that Ansible gathers automatically from the server, containing its default network IP address.

Now, the system administrator runs a single Ansible playbook that loops through all the web servers in the inventory. For each server, Ansible:

Uses the 'template' module to process the 'monitoring_agent.conf.j2' file.

Replaces {{ inventory_hostname }} with the real hostname of that server, e.g., 'web-01.fastshop.com'.

Replaces {{ ansible_default_ipv4.address }} with the server's actual IP, e.g., '10.0.1.100'.

Copies the resulting customised file to the server at '/etc/monitoring/agent.conf'.

The result: Every single web server gets a perfectly accurate configuration file, no manual edits, no typos, no drift. When FastShop adds 50 new servers to handle a holiday rush, they just add the servers to the Ansible inventory and run the playbook. The templates handle the rest.

This process is critical for maintaining a stable, scalable production environment. It allows the operations team to change parameters across the entire fleet of servers by simply updating the template file and re-running the playbook. Changing a global port number or adding a new configuration directive takes minutes, not days.

How EX294 Actually Tests This

The EX294 exam tests your practical ability to write and use Jinja2 templates within Ansible. This is not a theoretical test; you will be expected to write template files and the Ansible tasks that use them.

The core exam topics you must master include:

The 'template' module: You must know the syntax for using the ansible.builtin.template module, including the 'src', 'dest', 'owner', 'group', and 'mode' parameters. You will be asked to write a task that uses this module.

Variable syntax: You must know the difference between using {{ }} for expressions and {% %} for statements. A common trap is using the wrong delimiters, like using {{ }} inside a for loop, which is incorrect.

Conditionals ('if'): You must be able to write {% if <condition> %} ... {% endif %} to include content only when a condition is true.

Loops ('for'): You must be able to write {% for item in list %} ... {% endfor %} to generate repeated blocks of configuration.

Filters: You will be expected to use filters like 'default', 'upper', 'lower', and 'join' to manipulate variables within the template. The most common filter tested is the 'default' filter, which provides a fallback value if a variable is undefined. Its syntax is {{ variable | default('fallback_value') }}.

Facts: You need to know how to reference system facts like {{ ansible_facts['default_ipv4']['address'] }}. The exam loves to test if you can access nested dictionary values.

Common traps that EX294 sets for candidates:

Trap 1: Forgetting the '.j2' extension. The exam environment expects template files to have the .j2 extension. If you name it just 'template.conf', it will not be recognised.

Trap 2: Using incorrect indentation in the template. Jinja2 syntax requires careful indentation. A mismatched 'endif' or 'endfor' will cause the template to fail.

Trap 3: Confusing 'src' and 'dest'. The 'src' is the path to the template on the control node (your Ansible machine), and 'dest' is the path on the managed node (target server).

Trap 4: Not using the 'default' filter. If a variable is not defined and you do not use a default filter, the template will fail with an 'undefined variable' error. The exam expects you to handle this correctly.

Trap 5: Misunderstanding the difference between host variables and group variables. You must know that template variables are resolved against the specific host being configured, so group-level variables are available.

To be safe, memorise this syntax pattern: '{{ ansible_facts['<fact_name>'] }}' for accessing network facts. Practise writing a full playbook that uses a template with a for loop and an if statement. The exam will almost certainly ask you to combine multiple Jinja2 features in one template.

Key Takeaways

Jinja2 replaces manual editing of configuration files, saving time and preventing errors across hundreds of servers.

The 'template' module in Ansible takes a source file (.j2) and renders it on a managed node using variables specific to that host.

Use double curly braces {{ }} for variable expressions and curly brace percent signs {% %} for logic statements like 'if' and 'for'.

A variable that is undefined will cause the template to fail unless you use the 'default' filter, e.g., '{{ variable | default("fallback") }}'.

Ansible facts are accessible inside templates using the 'ansible_facts' dictionary, e.g., '{{ ansible_facts["default_ipv4"]["address"] }}'.

A single template file can generate unique configurations for every server in your inventory by referencing host-specific variables and facts.

The 'for' loop in Jinja2 allows you to generate repeated configuration blocks, such as defining multiple users or listening ports.

The 'if' statement in Jinja2 lets you conditionally include parts of the configuration based on a variable's value.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Template module

Processes Jinja2 variables and generates a unique file per host.

Requires a source file with .j2 extension (usually).

The output file on the target host is the fully rendered version.

Copy module

Copies a static file exactly as-is, no variable substitution.

Can copy any regular file, not just templates.

The output file is identical to the source file.

Jinja2 {{ }} delimiters

Used for variables and expressions that output a value.

Example: {{ server_name }} outputs the value of server_name.

Can include filters: {{ name | upper }}.

Jinja2 {% %} delimiters

Used for control flow statements like 'if', 'for', and 'set'.

Example: {% if condition %} includes content only if true.

Must be closed with {% endif %} or {% endfor %}.

Host variables

Defined in host_vars/ directory or inventory line.

Apply to a single specific host.

Highest priority for template resolution for that host.

Group variables

Defined in group_vars/ directory or inventory group.

Apply to all hosts in a group.

Lower priority than host variables; can be overridden.

Jinja2 'default' filter

Provides a fallback value if variable is undefined.

Syntax: {{ var | default('fallback') }}.

Does not cause an error; silently provides default.

Jinja2 'required' test

Causes the template to fail if variable is undefined.

Syntax: {% if var is not defined %}...{% endif %} or using a custom check.

Used when you want to enforce that a variable must exist.

Watch Out for These

Mistake

Jinja2 is a programming language that can replace Python in Ansible.

Correct

Jinja2 is a templating language, not a general-purpose programming language. It can only be used to generate text files. You cannot write Python functions or complex algorithms in Jinja2; it only does variable substitution, conditionals, and loops.

Beginners see the curly braces and similar syntax to Python and assume it is a subset of Python. They can be misled by the fact that Ansible itself is written in Python, but the template engine is its own separate domain-specific language.

Mistake

You must manually run the template on each server individually.

Correct

Ansible's 'template' module runs on all hosts in your inventory that the playbook targets. You write one task and Ansible automatically copies the rendered template to every host it manages, using each host's specific variables.

This misconception comes from thinking of templates as a manual copy-paste process. Beginners often do not grasp the automation aspect of Ansible, where one instruction applies to many hosts simultaneously.

Mistake

The template file must have the exact same name as the destination file to work.

Correct

The names are completely independent. You can have a template called 'config.j2' that generates a file called 'myapp.conf'. The 'src' and 'dest' parameters of the template module define these separately.

People confuse the source and destination files. They think the .j2 extension is part of the final filename, leading them to believe the output file must also end in .j2, which is not the case.

Mistake

You cannot use Ansible facts inside a Jinja2 template because facts are too complex.

Correct

Ansible facts are just variables in a dictionary format. You can access them directly in a template. For example, '{{ ansible_facts['os_family'] }}' works perfectly and is a common exam requirement.

The nested dictionary structure of facts (e.g., ansible_facts['default_ipv4']['address']) looks complex, so beginners think it is outside the scope of variables that templates can use. They also fear making syntax errors with the nested brackets.

Mistake

Jinja2 templates are only for configuration files, not for scripts or any other type of file.

Correct

Jinja2 templates can generate any text-based file: configuration files, scripts, HTML pages, YAML files, or Markdown documents. As long as the output is text, Jinja2 can create it. This is a common use for generating dynamic documentation.

The 'template' module is most often used for config files in the EX294 curriculum, so candidates falsely assume it is limited to that use case. They fail to see the broader utility.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

What is the difference between a static file and a template file in Ansible?

A static file (used with the 'copy' module) is an exact file that is copied verbatim to the server. A template file (used with the 'template' module) contains Jinja2 placeholders that Ansible replaces with variables to create a unique file for each host.

Do I need to install Jinja2 separately?

No, Jinja2 is automatically installed as a dependency of Ansible. When you install Ansible (for example, via 'dnf install ansible'), Jinja2 is included and ready to use.

What happens if a variable used in the template is not defined?

Ansible will fail with an 'undefined variable' error. To prevent this, you should use a default value with the 'default' filter, like '{{ my_variable | default("fallback_value") }}'. This provides a safe default if the variable is missing.

Can I use Jinja2 templates with the 'copy' module?

No, the 'copy' module does not process Jinja2 variables. You must use the 'template' module for that purpose. The 'copy' module is only for transferring static files.

How do I create a for loop in a Jinja2 template?

You use the '{% for item in list %}' syntax. For example, to generate a line for each user: '{% for user in users %} user {{ user }} exists {% endfor %}'. You must close the loop with '{% endfor %}'.

Can I use Jinja2 templates to generate YAML files for other Ansible tasks?

Yes, you can. You could generate a dynamic inventory file or a configuration file that is itself a YAML document. The template can produce any text format, including YAML, JSON, or plain text.

What is the difference between {{ ansible_facts['eth0']['ipv4']['address'] }} and {{ ansible_eth0['ipv4']['address'] }}?

Both access the same fact, but the 'ansible_eth0' shorthand is deprecated in newer Ansible versions. You should use '{{ ansible_facts["eth0"]["ipv4"]["address"] }}' because it is compatible with Ansible 2.10 and later, and is the recommended style for the EX294 exam.

Terms Worth Knowing

Keep going

You've finished Templates Using Jinja2. Continue through the EX294 study guide to build a complete picture of the exam.

Done with this chapter?