The Template Trap
How network configuration templates silently evolve into unstructured, untested code, and why the escape path leads to native programming languages.
The promise
Templates for router and switch configuration are one of the first tools network teams reach for when they begin automating. The appeal is obvious: write the configuration once as a template, inject device-specific variables, and render consistent configs across hundreds or thousands of devices. Jinja2, Go templates, Mako, ERB. The options are plentiful and the learning curve feels gentle.
For simple cases, templates deliver on their promise. A handful of variables, a few conditional blocks, and the result is a repeatable configuration that eliminates copy-paste errors. The trouble starts when "simple" stops being an accurate description of your network.
The complexity conundrum
Networks grow. They diversify across vendors, hardware generations, protocol stacks, and deployment contexts. Each new requirement adds logic to the template: another conditional, another loop, another variable interpolation. What began as a readable configuration snippet gradually becomes a program in disguise.
Consider a template that must handle both Cisco and Juniper devices, configure OSPF and BGP conditionally, and iterate over interfaces with vendor-specific syntax:
{# Variable declarations #} {% set vendors = ["cisco", "juniper"] %} {% set ospf_enabled = True %} {% set bgp_enabled = True %} {% for device in devices %} # Configuration for {{ device.hostname | upper }} {% if device.vendor == "cisco" %} hostname {{ device.hostname }} {% for interface in device.interfaces %} interface {{ interface.name }} description {{ interface.desc }} ip address {{ interface.ip }} no shutdown {% endfor %} {% if ospf_enabled %} router ospf 1 {% for interface in device.interfaces %} network {{ interface.ip.split('/')[0] }} 0.0.0.255 area 0 {% endfor %} {% endif %} {% if bgp_enabled %} router bgp 65001 bgp router-id {{ device.interfaces[0].ip.split('/')[0] }} neighbor 192.0.2.1 remote-as 65002 {% endif %} {% elif device.vendor == "juniper" %} set system host-name {{ device.hostname }} {% for interface in device.interfaces %} set interfaces {{ interface.name }} description "{{ interface.desc }}" set interfaces {{ interface.name }} unit 0 family inet address {{ interface.ip }} {% endfor %} {% if ospf_enabled %} set protocols ospf area 0.0.0.0 interface all {% endif %} {% if bgp_enabled %} set protocols bgp group external type external set protocols bgp group external neighbor 192.0.2.1 peer-as 65002 set protocols bgp group external export ["advertise-routes"] {% endif %} {% endif %} # End of configuration for {{ device.hostname }} {% endfor %}
This is no longer a template. It is a program written in a language that has no compiler, no type system, no debugger, and no test framework. Every {% if %} is a branch. Every {% for %} is a loop. Every .split('/')[0] is a string manipulation that will fail silently if the input format changes. The template has all the complexity of code with none of the safety nets.
Templates become code the moment they contain conditionals and loops. But they never gain the tooling, the testing, or the discipline that code demands. The result is unstructured, untested logic governing production infrastructure.
Four problems that compound
Logic overload
Conditional statements, loops, and variable interpolations multiply to accommodate vendor differences, protocol variations, and deployment contexts. Engineers struggle to predict how a change in one branch affects the rendered output. Misconfigurations become inevitable.
Readability decay
Complex templates are notoriously difficult to read and maintain. When a template resembles code more than a configuration snippet, it demands programming rigor: the very rigor that the template structure was designed to avoid. The logic becomes convoluted, difficult to understand, and fragile to change over time.
No testing framework
Templates effectively become code as they incorporate programming constructs, yet they lack robust testing practices. Unit tests, integration tests, and code reviews are seldom applied. Bugs hide until production, where a minor template change can cascade into a network outage.
Poor composition
Templates suffer from inadequate structuring. There is minimal use of modularization, where smaller reusable components build up the overall configuration. Monolithic templates are brittle: a change for one scenario may require modifications throughout the entire template.
The illusion of code without testing
Perhaps the most dangerous aspect of the template trap is the testing gap. In software engineering, the relationship between code and tests is non-negotiable. Every function has a unit test. Every integration point has a contract test. Every deployment goes through a CI/CD pipeline that validates correctness before reaching production.
Templates exist outside this discipline. They are rendered, not compiled. They produce text, not typed structures. There is no natural place to write a unit test for a Jinja2 block. There is no linter that understands the semantics of a network configuration. There is no debugger that lets you step through a template's execution path while observing the state of each variable.
If your template contains conditionals and loops, it is code. If it is code, it needs tests. If you cannot test it, you should not be running it in production.
The missing tool: feature flags
In software development, feature flags allow teams to enable or disable features without deploying new code, facilitating gradual rollouts and A/B testing. In the realm of network templates, implementing feature flags is cumbersome at best and dangerous at worst.
Without easy mechanisms for progressive rollouts, deploying new network features becomes an all-or-nothing proposition. A change to a shared template affects every device that references it, simultaneously. There is no way to roll out a new BGP policy to 5% of routers first and watch for regressions before widening the blast radius.
A single error in a shared template propagates to every device that consumes it. Without feature flags or progressive rollout mechanisms, the blast radius of a mistake is the entire fleet.
The escape: structured configuration in native code
The alternative is not to abandon automation. It is to stop pretending that templates are not code and instead write actual code with the full power and discipline of a native programming language: Go, Python, Rust, Java, or any language with a compiler, a type system, a test framework, and a debugger.
Consider the same multi-vendor configuration expressed as structured code:
for _, device := range devices { scc.SetComment(fmt.Sprintf( "Configuration for %s: %s", strings.ToUpper(device.Vendor), device.Hostname, )) // General settings scc.Setf("hostname %s", device.Hostname) // Interfaces for _, iface := range device.Interfaces { scc.PushSection(fmt.Sprintf("interface %s", iface.Name)). Setf("description %s", iface.Description). Setf("ip address %s", iface.IP) if device.Vendor == "cisco" { scc.Set("no shutdown") } scc.Pop() // Ends interface section } // OSPF if device.OSPFEnabled { scc.PushSection("router ospf 1") for _, iface := range device.Interfaces { if device.Vendor == "cisco" { scc.Setf("network %s 0.0.0.255 area 0", strings.Split(iface.IP, " ")[0]) } else if device.Vendor == "juniper" { scc.PushSection("protocols ospf"). Set("area 0.0.0.0 interface all"). Pop() } } scc.Pop() // Ends OSPF } // BGP if device.BGPEnabled { if device.Vendor == "cisco" { scc.PushSection("router bgp 65001"). Setf("bgp router-id %s", device.BGPRouterID). Setf("neighbor %s remote-as %d", device.BGPNeighbor, device.BGPRemoteAS). Pop() } else if device.Vendor == "juniper" { scc.PushSection("protocols bgp group external"). Set("type external"). Setf("neighbor %s peer-as %d", device.BGPNeighbor, device.BGPRemoteAS). Set(`export ["advertise-routes"]`). Pop() } } scc.SetComment(fmt.Sprintf( "End of configuration for %s", device.Hostname, )) }
The logic is identical. The output is the same. But every dimension of the engineering experience has changed.
What native code gives you
The structural comparison
Investing in developer practices
Moving from templates to native code is not just a technical shift. It is a cultural one. Network teams must adopt software development best practices: code reviews where peers catch errors early, CI/CD pipelines that test and deploy changes safely, and documentation that aids understanding and onboarding.
This is not asking network engineers to become software developers. It is recognizing that the moment configuration management involves conditionals, loops, and multi-vendor logic, it is software development. The choice is between doing it with the right tools or doing it without them.
The goal is not to abandon templates entirely. Simple, logic-free templates still have their place. The goal is to recognize the moment a template crosses the threshold into code, and to treat it accordingly: with a compiler, a test suite, a debugger, and the discipline of software engineering.
The better answer: model-driven configuration
Structured configuration in native code is a significant step forward, but the real destination is to stop writing configuration logic altogether and instead derive it from a model. This is exactly what YANG was designed to do.
YANG (RFC 7950) is a data modeling language for network configuration and state. Rather than writing templates or imperative code that renders CLI commands, you define a schema: the structure, types, constraints, and relationships of the configuration data. The device then consumes structured data (XML or JSON) that conforms to that schema via NETCONF or RESTCONF. No string manipulation, no vendor-specific syntax, no text rendering.
YANG inverts the problem. Instead of "how do I render this configuration," the question becomes "what is the shape of the data the device expects." The model defines the contract. The data conforms to it. The device interprets it.
The promise of YANG is substantial. OpenConfig, a vendor-neutral initiative, has built a library of YANG models for common network functions: interfaces, BGP, OSPF, MPLS, QoS. The IETF has standardized NETCONF (RFC 6241) and RESTCONF (RFC 8040) as the transport protocols. In principle, the same structured data should configure any compliant device regardless of vendor.
Why YANG has not won yet
In practice, the YANG ecosystem has struggled to reach critical mass, and the primary reason is tooling. The models exist. The protocols exist. What is missing is the developer experience that makes them productive to use at scale.
Tooling gap
There is no mature, widely adopted YANG toolchain comparable to what software developers expect: no great IDE integration, limited code generation, rudimentary validation libraries. Working with YANG models often feels like working against them.
Vendor deviations
Despite OpenConfig's efforts, vendors implement YANG models inconsistently. Augmentations, deviations, and proprietary extensions fragment the "universal standard" promise. The same BGP configuration requires different YANG paths on different platforms.
Model coverage
OpenConfig and IETF models cover common features well, but advanced or vendor-specific functionality often falls outside their scope. Teams end up maintaining a mix of standard and native models, losing the consistency benefit.
Testing and simulation
Testing YANG-based configurations against a model is possible in theory, but the tooling for offline validation, simulation, and dry-run execution remains immature compared to what a native language's test framework provides.
The irony is that YANG solves the right problem. It moves configuration from imperative rendering to declarative data. It separates the "what" (the configuration intent) from the "how" (the device-specific implementation). It is, conceptually, the ideal bridge between templates and true model-driven infrastructure. But without a first-class developer experience, adoption stalls, and teams fall back to templates or native code because those tools, however imperfect, at least work reliably today.
A good data model without good tooling is an academic exercise. OpenConfig and the IETF have done the hard work of defining the models. What the industry still lacks is the tooling ecosystem that makes those models productive, testable, and pleasant to work with at scale.
The full picture
This shift connects directly to the broader evolution described in Infrastructure as Code, Infrastructure as Data. Templates are generation one: config file rendering. Structured configuration in native code is generation two: actual Infrastructure as Code. YANG and model-driven configuration point toward generation three: Infrastructure as Data. The next generation, Intent-Driven Infrastructure, goes further still, from declarative models to autonomous systems that converge the network toward a declared intent. But the first step is escaping the template trap.