Lesson 3 of 5

Jinja2 Templates for Network Config

Jinja2 Templates for Network Config

Introduction

As your network grows from a handful of devices to dozens or hundreds, manually writing configuration files for each switch and router becomes unsustainable. A single VLAN change across fifteen leaf switches means editing fifteen separate configuration snippets, and every manual edit is a chance for a typo that brings down a segment. Jinja2 templating solves this problem by letting you define a configuration pattern once and then automatically generate device-specific configs from structured data.

In this lesson you will learn how Jinja2 templates integrate with Ansible to produce network configurations at scale. By the end, you will be able to:

  • Explain what Jinja2 templates are and why they matter for network automation.
  • Write a Jinja2 template that loops through variable data to build VLAN configurations.
  • Understand how Ansible roles, variable files, and templates connect to form a complete automation workflow.
  • Distinguish between legacy and resource modules for NX-OS and know when to fall back to the config or command modules.

Key Concepts

What Is Jinja2?

Jinja2 is a text-based templating engine used by Ansible. Instead of hard-coding device-specific values such as VLAN IDs, VNI numbers, or IP addresses directly into a playbook task, you place placeholders inside double curly braces — {{ variable }} — and Jinja2 fills them in at run time from your variable files.

Ansible Roles and Directory Structure

Ansible encourages you to organize automation work into roles. Each role has its own folders for tasks, templates, and variables. When you build a VXLAN fabric, for example, the directory layout looks like this:

DirectoryPurpose
roles/overlay/tasks/main.ymlThe task file that calls the template and pushes config
roles/overlay/templates/vlans.j2The Jinja2 template that generates VLAN payload
group_vars/leafs.ymlVariables shared by all leaf switches
host_vars/10.15.1.13.ymlVariables unique to a single leaf switch

This separation means your automation logic lives in the task file, your configuration pattern lives in the template, and your per-device or per-group data lives in variable files. Change the data and the config follows automatically.

Variable Quoting Rule

One detail that trips up many beginners: if a YAML value starts with {{ }}, you must wrap the entire expression in quotes. Ansible parses YAML before it evaluates Jinja2 expressions, and an unquoted opening brace is invalid YAML. For example:

# Correct
config: "{{ nxos_vlans | from_yaml }}"

# Incorrect — will cause a YAML parse error
config: {{ nxos_vlans | from_yaml }}

Legacy Modules vs. Resource Modules

Ansible provides two families of modules for configuring NX-OS devices. Understanding the difference helps you choose the right approach when templates alone are not enough.

AttributeLegacy ModulesResource Modules
Consistency across platformsInconsistent across different network devicesConsistent across different network devices
Handling multiple itemsRequires task loops for more than one configuration itemCan leverage task loops or Jinja2 templating for config blocks
State managementSimple states: present or absentIntroduces new states for Ansible to be the source of truth

Resource modules are the preferred choice for modern automation because they let Ansible act as the source of truth for your network configuration, with richer state options beyond simple present/absent.

How It Works

The Jinja2 workflow in Ansible follows a clear sequence: define your data, write a template, render the template into a configuration payload, and push that payload to the device.

Step 1 — Define the Data

Variable data is stored in YAML files. Group-level variables apply to every device in a group, while host-level variables apply to a single device. Here is an example of a group variable file for leaf switches:

# group_vars/leafs.yml
networks:
  - vrf_name: AnsibleVRF
    vlan_name: AnsibleNet1
    vlan_id: 101
    vni_id: 10101
    ip: 10.1.101.1/24
  - vrf_name: AnsibleVRF
    vlan_name: AnsibleNet2
    vlan_id: 102
    vni_id: 10102
    ip: 10.1.102.1/24

A host variable file adds device-specific values such as the hostname:

# host_vars/10.15.1.13.yml
hostname: L1

Step 2 — Write the Jinja2 Template

The template uses a {% for %} loop to iterate over the networks list and produce one block of YAML output per network entry:

# roles/overlay/templates/vlans.j2
{% for network in networks %}
- vlan_name: {{ network.vlan_name }}
  vlan_id: {{ network.vlan_id }}
  vni_id: {{ network.vni_id }}
{% endfor %}

When Ansible renders this template against the variable data above, it produces a list of two VLAN definitions — one for VLAN 101 and one for VLAN 102 — without you writing either definition by hand.

Step 3 — Render and Push

The task file in the role ties everything together. It first uses ansible.builtin.set_fact with the lookup('template', ...) function to render the Jinja2 template into a variable. Then it passes that variable to the cisco.nxos.nxos_vlans resource module:

# roles/overlay/tasks/main.yml
- name: Generate VLAN Config Payload
  ansible.builtin.set_fact:
    nxos_vlans: |
      "{{ lookup('template', 'vlans.j2') }}"

- name: Configure VLANs
  cisco.nxos.nxos_vlans:
    config: "{{ nxos_vlans | from_yaml }}"
    state: merged

The state: merged directive tells Ansible to merge the generated configuration with whatever already exists on the switch. This is one of the richer state options that resource modules provide.

Step 4 — The Playbook Orchestrates Roles

A top-level playbook assigns roles to device groups from the inventory:

# vxlan.yml
- hosts: spines, leafs
  gather_facts: false
  roles:
    - role: common
    - role: underlay

- hosts: leafs
  gather_facts: false
  roles:
    - role: overlay

The inventory file maps group names to device IP addresses:

# inventory.yml
all:
  children:
    spines:
      hosts:
        10.15.1.11:
        10.15.1.12:
    leafs:
      hosts:
        10.15.1.13:
        10.15.1.14:
        10.15.1.15:

You execute the entire workflow with a single command:

ansible-playbook -i inventory.yml vxlan.yml

Ansible reads the inventory, matches hosts to plays, loads group and host variables, renders templates, and pushes configurations to every device in the correct order.

Configuration Example

Fallback: The nxos_config Module

Sometimes there is no dedicated resource module for the feature you need to configure. In that case, the cisco.nxos.nxos_config module lets you pass raw CLI lines directly:

- name: Configure PIM Anycast RP
  cisco.nxos.nxos_config:
    lines:
      - "ip pim anycast-rp {{ s1_loopback1 }} {{ s1_loopback0 }}"
      - "ip pim anycast-rp {{ s2_loopback1 }} {{ s2_loopback0 }}"
    save_when: modified

Notice the Jinja2 variables inside the CLI lines — even when you drop down to raw config mode, you still benefit from templating. The save_when parameter controls when a copy running-config startup-config is performed:

save_when ValueBehavior
alwaysCopy always
modifiedCopy only if config changed since last save
changedCopy only if the task made a change
neverNever copy

Fallback: The nxos_command Module

For operational commands such as show outputs, use cisco.nxos.nxos_command:

- name: Get Show Commands
  cisco.nxos.nxos_command:
    commands:
      - show version
      - show ip ospf neighbor
      - show ip pim neighbor

This module also supports interactive prompts. When a command asks for a username and password, you can supply answers automatically:

- name: Misc Commands
  cisco.nxos.nxos_command:
    commands:
      copy ftp://nxos.bin bootflash:
    prompt:
      - "Username:"
      - "Password:"
    answer:
      - <username>
      - <password>

Best Practice: Use resource modules whenever one exists for the feature you are configuring. Fall back to nxos_config for CLI-only features and nxos_command for show or file-transfer commands. This layered approach keeps your automation as structured as possible while still covering edge cases.

Real-World Application

Data Center Fabric Deployment

The most common production use case for Jinja2 templates is deploying VXLAN EVPN fabrics. A data center may have two spine switches and dozens of leaf switches. Every leaf needs the same set of VLANs, VNIs, and VRF mappings, but with different loopback IPs and hostnames. By placing shared network definitions in group_vars/leafs.yml and device-unique values in individual host_vars files, a single ansible-playbook run configures the entire fabric consistently.

Branch Office Rollouts

The reference lab topology includes a core XR router, branch Cat8Kv routers, and NX-OS switches. Jinja2 templates work the same way here: define interface IPs and OSPF areas in variable files, write templates for interface and routing configuration, and deploy across all branch devices with one playbook. When a second branch is added, you reuse the same playbooks with new variable files — no logic changes required.

Ansible Configuration File Lookup Order

When running playbooks, Ansible searches for its configuration file ansible.cfg in a specific order:

  1. The path set in the ANSIBLE_CONFIG environment variable
  2. The current working directory
  3. The user's home directory
  4. /etc/ansible

Important: Always execute your playbooks from the directory that contains your project's ansible.cfg. Running from the wrong directory can cause Ansible to pick up a different configuration file, leading to unexpected behavior such as wrong inventory paths or missing module settings.

Design Considerations

  • Indentation matters. YAML is whitespace-sensitive. When editing playbooks, use an editor that shows vertical indentation guides — a single extra or missing space can break a playbook.
  • Separate data from logic. Keep your variable files clean and your templates focused on structure. This makes it easy to onboard new devices by adding a variable file rather than editing automation logic.
  • Use roles for reusability. Roles like common, underlay, and overlay can be mixed and matched across playbooks. A branch deployment playbook and a data center playbook can share the same common role.

Summary

  • Jinja2 templates let you define a configuration pattern once and generate device-specific configs by looping through structured variable data with {% for %} blocks and {{ variable }} placeholders.
  • Ansible roles organize tasks, templates, and variables into reusable units — separating what to do, how to do it, and data to do it with.
  • Resource modules like cisco.nxos.nxos_vlans are preferred over legacy modules because they provide consistent behavior across platforms and richer state management including the merged state.
  • When no resource module exists, nxos_config handles raw CLI configuration and nxos_command handles show commands and interactive prompts.
  • Always run playbooks from the correct directory so Ansible finds the right ansible.cfg, and always quote YAML values that start with {{ }}.

In the next lesson, we will look at playbook optimization techniques including import_playbook for reusing playbooks across multiple sites and encrypting sensitive credentials with Ansible Vault.