> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Jinja2 Templates for Network Configs: From Variables to Rendered CLI
- URL: https://www.pinglabz.com/jinja2-network-config-templates/
- Published: 2026-07-12T09:54:07.000Z
- Updated: 2026-08-24T05:22:27.000Z
- Description: Stop hand-editing pasted router configs. This post builds the full templating pipeline: YAML variables, a Jinja2 template, rendered CLI, and a Netmiko push to a live router, all captured from a real CML lab.
- Author: Jaime
- Tags: Network Automation, Labs, CCIE, #Import 2026-08-01 19:54

Copying a router config, pasting it, and changing the IP addresses by hand is how configuration errors are born. Jinja2 templating replaces that with a template plus a data file: write the structure once, describe each device in a few variables, and render perfect, consistent configuration every time. This is the single most useful automation skill for a network engineer, and unlike some of this cluster, it runs on any platform and needs nothing exotic.

This article builds the complete pipeline - YAML variables, a Jinja2 template, rendered CLI, and a Netmiko push to a live router - all captured for real from a CML lab. It extends the [network automation guide](https://www.pinglabz.com/network-automation/).

## The idea: separate structure from data

Every device of a given type shares the same *structure* \- the same interfaces, the same protocols, the same command hierarchy. What differs is the *data*: the hostname, the IPs, the site ID. Templating separates the two:

- The **template** is the config structure with placeholders. Written once, reviewed once.
- The **data** (variables) describes each specific device. Small, readable, per-device.
- The **rendered output** is the template with the data filled in - the actual config to push.

Get the template right once, and every device rendered from it is correct by construction. No copy-paste drift, no forgotten line, no fat-fingered subnet mask.

## The data: YAML variables

YAML is the natural format for the variables - human-readable, structured, and what Ansible and most automation tooling expect. From the lab, describing two loopbacks and an OSPF process:

```
loopbacks:
  - {id: 100, ip: 172.20.100.1, mask: 255.255.255.0, desc: JINJA-AUTOMATION-100}
  - {id: 101, ip: 172.20.101.1, mask: 255.255.255.0, desc: JINJA-AUTOMATION-101}
ospf:
  process: 10
  networks:
    - {net: 172.20.100.0, wild: 0.0.0.255, area: 0}
    - {net: 172.20.101.0, wild: 0.0.0.255, area: 0}
```

That is the whole per-device description. Readable, reviewable, and trivial to keep in version control - a diff on this file shows exactly what changed about the device.

## The template: Jinja2

Jinja2 is a templating language with loops, conditionals, and variable substitution. The template mirrors the config structure, iterating over the data:

```
{% for lo in loopbacks %}
interface Loopback{{ lo.id }}
 description {{ lo.desc }}
 ip address {{ lo.ip }} {{ lo.mask }}
{% endfor %}
router ospf {{ ospf.process }}
{% for n in ospf.networks %} network {{ n.net }} {{ n.wild }} area {{ n.area }}
{% endfor %}
```

`{{ }}` substitutes a variable; `{% for %}` loops. The template says "for each loopback in the data, emit an interface block" - so whether the data has two loopbacks or two hundred, the template is the same. That loop is the leverage: one template, any number of instances.

## The rendered output

Feed the YAML data through the Jinja2 template and out comes real, pushable CLI - captured from the lab:

```
interface Loopback100
 description JINJA-AUTOMATION-100
 ip address 172.20.100.1 255.255.255.0

interface Loopback101
 description JINJA-AUTOMATION-101
 ip address 172.20.101.1 255.255.255.0

router ospf 10
 network 172.20.100.0 0.0.0.255 area 0
 network 172.20.101.0 0.0.0.255 area 0
```

Perfect, consistent, and generated - not typed. The Python that does the rendering is three lines:

```
import yaml, jinja2
data = yaml.safe_load(open('vars.yml'))
rendered = jinja2.Template(open('template.j2').read()).render(**data)
```

## The push: Netmiko to a live device

Rendering is half the pipeline. The other half is getting it onto the router. Netmiko - a Python library that wraps SSH to network devices - takes the rendered config and pushes it, then reads back the result. From the lab, driving a real IOS XE router:

```
from netmiko import ConnectHandler
dev = {"device_type":"cisco_ios","host":"192.168.99.1",
       "username":"plauto","password":"..."}
conn = ConnectHandler(**dev)
out = conn.send_config_set(rendered.splitlines())
print(conn.send_command("show ip interface brief | include Loopback10"))
```

And the device, after the push, verified by the same script:

```
Loopback100  172.20.100.1  YES manual up  up
Loopback101  172.20.101.1  YES manual up  up

Interface  PID  Area  IP Address/Mask     Cost  State
Lo100      10   0     172.20.100.1/24     1     LOOP
Lo101      10   0     172.20.101.1/24     1     LOOP
```

**That is the full pipeline, end to end, real: YAML data → Jinja2 render → Netmiko push → live config → verified back from the device.** No hand-typing at any step. And critically, this runs on *any* platform - the router just needs SSH. No Guest Shell, no IOx, no DMI. It is the automation that works everywhere.

## Why this pipeline is the foundation

Everything more advanced builds on this pattern:

- **Ansible** is essentially this pipeline with structure, inventory, and idempotency added - it uses Jinja2 templates and pushes over SSH/NETCONF exactly like this.
- **CI/CD for networks** is this pipeline in a git repository with a test stage - the YAML vars are reviewed in a pull request, rendered, tested, and pushed by a pipeline.
- **Infrastructure as code** is the philosophy this embodies: the network's config is defined by data files in version control, and rendered/pushed by tooling, not typed by hand.

Master this small pipeline and you understand the mechanism behind all of it. The advanced tools are conveniences layered on this exact idea.

## Practical Jinja2 patterns

**Conditionals** `{% if intf.dhcp %}ip address dhcp{% else %}ip address {{ intf.ip }} {{ intf.mask }}{% endif %}` \- render different config based on the data. 

**Filters** `{{ hostname | upper }}`, `{{ vlans | join(',') }}` \- transform values inline. 

**Defaults** `{{ mtu | default(1500) }}` \- a fallback when the variable is absent. 

**Whitespace control** `{%- ... -%}` trims whitespace - important for clean config output without stray blank lines. 

## Guardrails

- **Review the rendered output before pushing.** Render to a file, eyeball it (or diff it against the current config), *then* push. A template bug rendered across a hundred devices is a hundred-device outage.
- **Version-control the data and templates.** The whole point is that config changes are data changes, reviewable in a pull request.
- **Test with a lab device first.** Push to one router, verify, then roll out - exactly as we did here.
- **Keep templates modular.** One template per function (interfaces, routing, security) composed together, rather than one giant template - easier to review and reuse.

## Key takeaways

- Jinja2 templating separates config **structure** (the template, written once) from **data** (per-device variables), so every rendered config is correct by construction.
- **YAML** holds the variables - readable and version-controllable. **Jinja2** holds the template, with loops and conditionals. **Netmiko** pushes the rendered config over SSH.
- Verified end to end in the lab: YAML → Jinja2 render → Netmiko push → live config → verified back from a real IOS XE router.
- This pipeline runs on **any platform** \- it just needs SSH. No Guest Shell, no IOx, no NETCONF required.
- It is the **foundation** for Ansible, network CI/CD, and infrastructure-as-code - all of which are this exact pattern with more structure around it.
- Always render to a file and review before pushing; version-control the data and templates; test on a lab device first.

Next: [JSON vs XML vs YAML for network engineers - one payload, three ways](https://www.pinglabz.com/json-xml-yaml-network-engineers/). The full cluster index lives on the [network automation pillar](https://www.pinglabz.com/network-automation/).