VyOSLIV · API and AutomationAutomation
Configuration as code — Git repository, Jinja2 templates, render and apply
What you'll learn
- Build a Git repository that holds the router configuration as Jinja2 templates
- Render the templates to produce the desired VyOS configuration
- Apply the rendered configuration via the HTTP API or Ansible
- Recognise the production failure modes where render-and-apply drifts from the source
Prerequisites
Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-15
Configuration as code is the practice of treating the router configuration as source code: stored in a Git repository, rendered through templates, and applied through a pipeline. The configuration becomes reviewable, testable, and recoverable in a way that a free-form configuration never is.
On VyOS 1.5 LTS, the typical configuration-as-code pipeline has four stages:
- Author — the operator edits a Jinja2 template and commits to Git.
- Render — a CI/CD job renders the template to a complete VyOS configuration document.
- Review — the diff between the previous and the rendered configuration is reviewed by a peer.
- Apply — the rendered configuration is applied to the router via the HTTP API or Ansible.
This lesson covers the Git repository structure, the Jinja2 templates, the render-and-apply pipeline, and the production failure modes where the render is correct but the apply is wrong.
The Git repository structure
The configuration repository has three top-level directories:
vyos-config/
├── inventory.yml # List of routers and their variables
├── group_vars/ # Variables applied to groups of routers
│ ├── all.yml
│ ├── edge.yml
│ └── core.yml
├── host_vars/ # Variables applied to individual routers
│ ├── edge-01.yml
│ ├── edge-02.yml
│ └── core-01.yml
└── templates/ # Jinja2 templates
├── base.j2 # System configuration (hostname, DNS, NTP)
├── interfaces.j2 # Interface configuration
├── routing.j2 # BGP / OSPF / static routes
└── firewall.j2 # Firewall rules
The inventory.yml file lists the routers and groups
them:
all:
children:
edge:
hosts:
edge-01:
ansible_host: 10.99.0.1
edge-02:
ansible_host: 10.99.0.2
core:
hosts:
core-01:
ansible_host: 10.99.0.10
The host_vars/ directory contains variables for each
router:
# host_vars/edge-01.yml
hostname: edge-01
loopback: 10.99.1.1/32
asn: 64512
router_id: 10.99.1.1
bgp_neighbors:
- ip: 10.0.0.1
remote_as: 65001
description: "Upstream A"
The templates/ directory contains Jinja2 templates that
render the configuration.
Jinja2 templates
A Jinja2 template is a text file with placeholders that are replaced by variable values when rendered:
{# templates/routing.j2 #}
set protocols bgp {{ asn }} parameters router-id {{ router_id }}
set protocols bgp {{ asn }} neighbor {{ item.ip }} remote-as {{ item.remote_as }}
set protocols bgp {{ asn }} neighbor {{ item.ip }} description "{{ item.description }}"
{% for neighbor in bgp_neighbors %}
set protocols bgp {{ asn }} neighbor {{ neighbor.ip }} remote-as {{ neighbor.remote_as }}
set protocols bgp {{ asn }} neighbor {{ neighbor.ip }} description "{{ neighbor.description }}"
{% endfor %}
The template iterates over the bgp_neighbors list and
produces one set command per neighbour. The rendered
output is a complete list of set commands that can be
applied via the HTTP API or Ansible.
Render and apply
The render step produces the configuration document; the apply step pushes it to the router:
flowchart LR
GIT[Git repository] -->|Jinja2| RENDER[Render\ntemplate + vars]
RENDER --> CONFIG[Configuration\ndocument]
CONFIG --> REVIEW{Peer review}
REVIEW -->|approved| APPLY[Apply\nHTTP API / Ansible]
APPLY --> ROUTER[Router]
REVIEW -->|rejected| FIX[Fix template\ncommit again]
The diagram shows the pipeline: Git is the source of truth; render produces the configuration; review catches errors; apply pushes to the router. The pipeline is the production alternative to free-form configuration.
# Render the template with Jinja2
jinja2 -d host_vars/edge-01.yml templates/routing.j2 -o rendered/edge-01/routing.conf
# Apply the rendered configuration via the HTTP API
curl -u "automation-key:PLAINTEXTKEY" \
-X POST \
-H "Content-Type: application/json" \
-d @<(jq -R -s 'split("\n") | map(select(length > 0)) | map({op: "set", path: (. | split(" ") | .[1:] | map(. | split(".") | .))})' rendered/edge-01/routing.conf) \
https://10.99.0.1/config
The render-and-apply pipeline is typically automated in a CI/CD job (GitLab CI, GitHub Actions, Jenkins). The job runs on every commit to the main branch; the apply step runs after the review is approved.
The role of the configuration repository
The configuration repository is the source of truth for the router configuration. Every change to the router’s running configuration must come from a commit to the repository:
flowchart TB
OP[Operator\nedits template] -->|commit| GIT[Git repository]
GIT -->|CI/CD| RENDER[Render]
RENDER --> CONFIG[Configuration]
CONFIG --> REVIEW[Peer review]
REVIEW -->|approved| APPLY[Apply via HTTP API]
APPLY --> ROUTER[Router running config]
ROUTER -.->|drift detection| DRIFT[Drift report]
DRIFT -.->|alert| OP
The diagram shows the closed loop: the operator edits the template, the pipeline renders and applies, the router state matches the configuration. A drift detection step runs on a schedule (e.g. nightly) and compares the router state to the rendered configuration. Any divergence is flagged.
Failure modes
Template syntax error
The operator writes a Jinja2 template with a syntax error
(unclosed {% for %} block). The render step fails; the
pipeline aborts before apply.
Diagnostic:
jinja2.exceptions.TemplateSyntaxError: unexpected end of template
Fix: correct the template syntax. The render step is the operator’s first line of defence; a syntax error is caught at render time, not at apply time.
Render produces wrong configuration
The template renders correctly but produces the wrong configuration (e.g. wrong ASN, wrong neighbour IP). The review step catches it; the pipeline aborts before apply.
Fix: correct the template or the variables. The review step is the operator’s second line of defence.
Apply succeeds but router state diverges
The apply step succeeds (the HTTP API returns success) but the router state diverges from the rendered configuration (e.g. a manual change was made between render and apply, or a partial apply failed silently).
Diagnostic: drift detection (a nightly job that renders and compares to the router state) reports divergence.
Fix: investigate the divergence. If a manual change was made, commit it to the Git repository. If a partial apply failed, re-run the apply.
Apply breaks the router
The apply step succeeds but the configuration is wrong (e.g. wrong OSPF area, wrong static route next-hop). The router’s routing protocols fail; traffic is blackholed.
Diagnostic: monitoring detects the traffic drop.
Fix: roll back to the previous configuration revision
(rollback N and commit). The Git repository is the
source of truth for the previous configuration; the
operator can re-render and re-apply the previous state.
Rollback
A configuration-as-code change is reversible through the standard VyOS mechanisms:
rollback Nandcommitto revert to a previous configuration revision.- Re-render the previous Git commit and re-apply.
The defensive idiom: every configuration-as-code pipeline keeps the rendered output in version control. The operator can recover the previous state by checking out the previous commit and re-running the apply.
Production discipline
Cross-course references
LIV-VyOS-Automation(vyos-liv-01-vyos-http-api,vyos-liv-02-vyos-ansible) covers the HTTP API and Ansible integration that the apply step uses.LIV-VyOS-Automation(vyos-liv-04-automated-validation, the next lesson) covers the validation patterns that complement the render-and-apply pipeline.- The Ansible course’s
XLI-Ansible-Networkcovers the Ansible patterns for network configuration.
Quiz
Knowledge check · 4 questions
Q1. What is the role of the Git repository in a configuration-as-code pipeline?
Q2. In a configuration-as-code pipeline, an operator may make a manual change to the router to fix an urgent issue, and commit the change to Git later.
Q3. An operator runs the configuration-as-code pipeline to apply a BGP configuration change to edge-01. The render step succeeds, the review approves, and the apply step succeeds. The next morning, the drift detection job reports that the router state diverges from the rendered configuration. The diff shows an OSPF network entry that is on the router but not in the rendered configuration. What is happening?
The configuration-as-code pipeline applied a BGP change to edge-01. The next morning, drift detection reports an OSPF network entry on the router that is not in the rendered configuration.
Q4. An operator commits a Jinja2 template change that introduces a typo (the wrong OSPF area ID). The render step succeeds, the review misses the typo (the operator was rushed), and the apply step succeeds. After 10 minutes, the operator notices the OSPF adjacency with the peer is down. What is happening and what is the fix?
A Jinja2 template change introduces a typo (wrong OSPF area ID). The render and apply succeed. After 10 minutes, the OSPF adjacency is down.
Passing score: 75%. Answers are checked in this browser.