Skip to main content
Infrastructure

Design-Driven Automation: When Your Architecture Document Runs the Infrastructure

I’d been staring at a hardcoded IP address in an Ansible defaults file for the third time that day, cross-referencing it against a table in the physical design document, when the thought hit me: why am I maintaining this in two places?

The design document had the IP. The Ansible variable had the IP. And yet, inevitably, one day they wouldn’t be the same. Someone (me) would update the design doc and forget the variable. Or update the variable and forget the doc. This is the fundamental lie of infrastructure documentation — we pretend it’s authoritative while the real config lives somewhere else entirely.

What if the design document wasn’t just documentation? What if it was the configuration?

The Problem With Two Sources of Truth

I maintain a nested VCF 9 lab with five TOGAF design documents — conceptual, logical, physical, delivery guide, and operations guide — all stored as markdown in git alongside the Ansible automation that builds the environment.

The physical design document has tables for everything: VLAN allocations, IP addresses, TEP pools, BGP peering configuration. Proper tables, with proper columns, reviewed and version-controlled. Exactly the kind of structured data that an infrastructure-as-code tool needs.

And then, separately, group_vars/all.yml had the same data. Different format, same information. Two files, one truth, zero guarantee they’d stay synchronised.

Every infrastructure team I’ve worked with has this problem. The design document says one thing. The Terraform state says another. The CMDB says a third. Someone asks “what IP is the NSX Manager on?” and you get three different answers depending on where you look.

What If the Table Was the Config?

I’d thought about this for months. It felt like an impossible dream — the kind of idea that sounds elegant in your head but crumbles the moment you try to implement it. Markdown is for humans, not machines. Design documents are prose, not configuration files.

Except markdown tables are surprisingly well-structured:

| ip | hostname | domain | role |
|----|----------|--------|------|
| 192.0.2.5 | sddc-manager | mgmt | SDDC Manager |
| 192.0.2.9 | vcenter-wld | wld | vCenter Server (workload) |

That’s parseable. A human can read it in a design review. A machine can read it in an Ansible playbook. Same file, same table, same row.

The insight was embarrassingly simple: use snake_case column names and write full values — no abbreviations, no en-dashes in IP ranges. The table becomes machine-readable with zero loss of human readability. The document doesn’t get uglier. It just gets more precise.

I added HTML comment tags above each table to mark them for the parser:

<!-- pd_hosts -->
| ip | hostname | domain | role |
|----|----------|--------|------|
| 192.0.2.5 | sddc-manager | mgmt | SDDC Manager |

The <!-- pd_hosts --> tag is invisible when the document renders. It’s a signal to the automation: this table is configuration, not just prose.

One Hundred Lines of Python

The Ansible vars plugin that makes this work is about 100 lines of Python. It finds HTML comment markers, parses the table that follows each one, and injects the rows as Ansible variables. Integer fields (VLAN IDs, MTU values, ASN numbers) get automatically cast. Everything else stays as strings.

No external dependencies. No complex markdown parsing library. Just splitting on | characters and stripping whitespace. The tables are simple enough that the parser can be simple too.

The plugin runs at playbook load time. Each tagged table becomes a list of dictionaries — column names become keys, cell values become values. This markdown:

<!-- pd_vlans -->
| vlan_id | name | subnet | gateway | mtu |
|---------|------|--------|---------|-----|
| 10 | management | 192.0.2.0/24 | 192.0.2.1 | 1500 |
| 40 | host_overlay | 198.51.100.0/24 | 198.51.100.1 | 8900 |

Becomes this Ansible variable:

pd_vlans:
  - {vlan_id: 10, name: management, subnet: "192.0.2.0/24", gateway: "192.0.2.1", mtu: 1500}
  - {vlan_id: 40, name: host_overlay, subnet: "198.51.100.0/24", gateway: "198.51.100.1", mtu: 8900}

Then any playbook can query it with standard Jinja2 filters:

# Get the SDDC Manager IP — straight from the design doc
"{{ (pd_hosts | selectattr('hostname', 'equalto', 'sddc-manager') | first).ip }}"

# Get all workload domain hosts
"{{ pd_hosts | selectattr('domain', 'equalto', 'wld') | list }}"

When I ran it for the first time and saw two dozen hosts, six VLANs, IP pools, and the BGP peers materialise as Ansible variables — parsed directly from the design document I’d been writing for months — I genuinely laughed. The impossible dream took about a minute to prove possible.

The Inversion

This inverts the usual documentation problem. Normally, documentation drifts from code because humans forget to update docs after changing config. With DDA — Design-Driven Automation — the document is the config. There’s nothing else to update. Change the IP in the design doc table, and the next Ansible run picks it up.

The flow is deliberately one-directional: document to automation, never the reverse. The vars plugin is read-only. It never writes back to the markdown. The design document remains the authority, and the automation is the consumer.

The design doesn’t describe the infrastructure. It defines it.

What Gets Parsed

What started as four tagged tables has grown to twelve as the lab did. The originals — pd_vlans (the lab’s VLANs with ID, subnet, gateway, MTU), pd_hosts (now nearly fifty rows: a twenty-host ESX plan plus every appliance, service address, and VIP), pd_ip_pools (NSX TEP and related pools), and pd_bgp (BGP peering with AS numbers) — were joined by pd_nsx_ip_blocks (the NSX VPC private blocks), pd_nsx_pinned_vips, pd_vapp_91, pd_bom_91, pd_vcd_layer, pd_vm_specs, and most recently pd_vcfa_ip_spaces and pd_vcfa_tenants as the design grew to pin the vApp build, the bill of materials, the vCloud Director layer, per-VM sizing, and the VCF Automation tenancy scheme.

The plugin parses every tagged table into variables; pd_hosts, pd_vlans, pd_ip_pools, pd_bgp, and pd_nsx_ip_blocks are the ones the playbooks consume as the single source of truth. Each phase’s DNS quality gate reads pd_hosts to ensure the right records exist; the bringup and workload-domain specs read pd_vlans, pd_ip_pools, and pd_nsx_ip_blocks for network configuration.

I didn’t invent new configuration. I reformatted what was already in the design document into something a machine could read.

The discipline now runs the other way too: a preflight gate scans the playbooks and roles and fails the build if any value from a tagged table is restated in code. The document isn’t just a source of truth — the automation refuses to let it stop being the only one.

Why TOGAF Matters Here

Each of the five design documents maps to a TOGAF ADM phase. When someone asks “why is the NSX Manager a single node?”, you trace it from the physical design table (the allocation) to the logical design decision (VCF-02: single-node NSX Manager) to the constraint behind it (C-004: minimal-footprint lab). The entire chain lives in git, versioned together, and now the physical design table at the bottom of that chain directly drives the automation that builds it.

What This Isn’t

It isn’t a replacement for Terraform state or Ansible inventory. Those manage runtime state — what’s actually deployed. DDA manages desired state — what the architecture says should exist. They’re complementary.

It also isn’t a general-purpose configuration management system. It works because the physical design has a specific, limited scope: IP addresses, VLAN IDs, hostnames, pool ranges. Things that change slowly and need to be traceable to design decisions. You wouldn’t put application feature flags in a TOGAF document.

And it’s definitely not production-grade yet. If someone puts a pipe character in a description field, it’ll break. That’s fine for a lab. But the principle holds.

The Broader Point

Every infrastructure team maintains design documents. Most of those documents contain tables of configuration data — IP schemes, VLAN allocations, firewall rules, DNS records. Those tables are already structured. They’re already reviewed. They’re already in version control.

The gap between “documentation that contains configuration” and “documentation that drives configuration” is smaller than you’d think. A few column name conventions. A few HTML comment tags. A hundred lines of Python.

The design document was always meant to be the authoritative source. DDA just takes that literally.


The lab project, including the physical design vars plugin and the full TOGAF document set, is at github.com/darrylcauldwell/dda-vcf. The Ansible vars plugin is at ansible/plugins/vars/physical_design.py.