Diagram-as-Code for Network Topology: D2 and Graphviz - 夜莺博客

Diagram-as-Code for Network Topology: D2 and Graphviz

Hand-drawn topology diagrams are out of date the day they are saved, and nobody can tell which diagram matches production. Diagram-as-code fixes the versioning problem: the diagram is a text file, it lives next to the configuration in git, and it is regenerated automatically whenever the source of truth changes. This article shows a working pipeline using D2 for the rendering and a small script that builds the topology file from an IPAM or Nautobot export.

Why Not Just Export a Screenshot

  • Text diffs: a pull request shows exactly which link was added, in a review anyone can read.
  • Consistency: the same source produces the L1, L2 and L3 views, so they never contradict each other.
  • Automation: a nightly job regenerates diagrams, so drift is visible as a commit rather than discovered during an incident.
  • No vendor lock-in: D2, Graphviz and Mermaid all render from plain text, and all can run in CI.

A D2 Topology Skeleton

# topology.d2
direction: right

core1: { class: spine }
core2: { class: spine }
leaf1: { class: leaf }
leaf2: { class: leaf }
fw1:   { class: firewall }

core1 <-> core2: 100G { style.stroke-width: 4 }
core1 <-> leaf1: 100G
core1 <-> leaf2: 100G
core2 <-> leaf1: 100G
core2 <-> leaf2: 100G
core1 <-> fw1: 40G

classes: {
  spine:    { style.fill: "#1f77b4" }
  leaf:     { style.fill: "#2ca02c" }
  firewall: { style.fill: "#d62728" }
}
d2 --layout=elk topology.d2 topology.svg
d2 --theme=200 --layout=elk topology.d2 topology.png
d2 --watch topology.d2 topology.svg       # live preview while editing

Generating the Source from Inventory

The valuable part is not the drawing, it is the data source. Devices and links can come from a text inventory, a CSV, or the API of a source-of-truth system:

import yaml, json, subprocess, pathlib

inv = yaml.safe_load(open('inventory.yml'))
lines = ['direction: right']
for d in inv['devices']:
    lines.append(f"{d['name']}: {{ label: "{d['name']}\\n{d['role']}" }}")
for l in inv['links']:
    if l.get('lag'):
        lines.append(f"{l['a']} <-> {l['b']}: {l['speed']} (LAG)")
    else:
        lines.append(f"{l['a']} <-> {l['b']}: {l['speed']}")
pathlib.Path('topology.d2').write_text('\n'.join(lines))
subprocess.run(['d2', '--layout=elk', 'topology.d2', 'docs/topology.svg'], check=True)

Point inventory.yml at the same data you use for NetBox IPAM or Nautobot as source of truth - a small exporter is enough; there is no need to hand-maintain two inventories.

Wire It Into CI

# .github/workflows/diagrams.yml
name: diagrams
on:
  push:
    paths: [inventory.yml, docs/topology.d2]
jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install D2
        run: curl -fsSL https://d2lang.com/install.sh | sh -s --
      - name: Render
        run: d2 --layout=elk docs/topology.d2 docs/topology.svg
      - name: Commit if changed
        run: |
          git config user.name "diagrams-bot"
          git add docs/topology.svg
          git diff --quiet --cached || git commit -m "docs: regenerate topology diagram"
          git push

Practical Rules

  • Keep one diagram per audience: the L1 physical view, the L3 routing view and the service view. Do not try to fit everything into one picture.
  • Annotate links with the real capacity and the interface name, not just a line - that is what makes a diagram useful during an outage.
  • Render to SVG, not PNG: SVG diffs meaningfully in git and stays readable at any zoom.
  • Treat a diagram that has not been regenerated in a month as drift, and alert on the pipeline failing rather than the file being stale.

Related: Batfish config validation validates the configuration the diagram claims to represent, and TextFSM parsing is how you turn device output into the inventory this pipeline consumes.

Common Pitfalls

  • Rendering is not validation. A diagram generated from a stale inventory is confidently wrong. The pipeline must read from the source of truth, not from a hand-edited file.
  • Do not nest everything. More than about thirty nodes on one canvas stops being readable; split by layer or by data centre.
  • Keep labels short. Link labels should carry interface and speed (Et49/1 100G), not full descriptions.
  • Fail loudly. If the render step errors, the CI job must fail rather than commit a half-rendered file.
  • One repository location. Diagrams belong next to the inventory they describe, so a reviewer sees both changes in a single diff.

原文链接:https://d2lang.com/tour/intro/