unkinben 585d32b15c
ci/woodpecker/pr/plan Pipeline failed
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/backfill-test Pipeline was successful
Backfill NetBox reality from pdbmux
Automate the PuppetDB reality import (issue #1) by querying pdbmux — the
PuppetDB multiplexer whose HTTPS gateway is reachable from CI/workstations,
unlike raw PuppetDB — instead of PuppetDB directly, and shaping the result to
the NetBox reality side the devices module reconciles.

- Add tools/backfill (Go): query pdbmux /pdb/query/v4/facts for the 13 existing
  physicals and emit per-host reality YAML — serial/model/UUID, every recordable
  interface (real NICs plus overlay/loopback/kube-lb) with MAC and CIDR IPs, and
  CPU/RAM/disk inventory. Filter ephemeral Calico veths and Ceph RBD volumes;
  take interface names from Facter, never assume them.
- Emit deterministic, idempotent, yamllint-clean output into
  config/au/syd1/reality/<host>.yaml, generated for prodnxsr0001-0013.
- Extend modules/infra with a reality variable and reality.tf creating
  netbox_device_interface/netbox_mac_address/netbox_ip_address/
  netbox_inventory_item and device serial; wire reality only for hosts that also
  have an intent device.
- Load reality/*.yaml in the terragrunt env; add `make backfill`; add a
  go vet/test woodpecker job; drop the in-cluster-only Python script.

Closes #1

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-06 23:15:21 +10:00
2026-08-06 23:15:21 +10:00
2026-08-06 23:15:21 +10:00

terraform-infra

YAML-driven Terraform/Terragrunt for infrastructure state that belongs in NetBox + Kea: subnets (NetBox prefixes + Kea DHCP scopes), logical networks, and physical devices. NetBox is authoritative for all IP/interface data; this repo declares intent and lets discovery/PuppetDB fill hardware reality.

Renamed from terraform-ipam — scope now spans devices and provisioning, not just IPAM. The old repo name is archived by the terraform-git rename.

Layout

config/<region>/<dc>/subnets/<name>.yaml    # one file per subnet (prefix + DHCP scope)
config/<region>/<dc>/networks/<name>.yaml   # logical networks devices join
config/<region>/<dc>/devices/<host>.yaml    # one file per device (intent only)
config/<region>/<dc>/reality/<host>.yaml    # one file per device (generated reality)
config/<region>/<dc>/managed-ips.yaml       # extra manually-managed IPs
environments/<region>/<dc>/terragrunt.hcl   # one Terragrunt env per region/dc
modules/infra/                              # the module
tools/backfill/                             # pdbmux -> reality YAML generator (Go)

make plan / make apply reconcile the YAML against NetBox + Kea. State lives in Consul under infra/terraform/infra/<region>/<dc>/state.

Subnet schema (subnets/<name>.yaml)

prefix: 198.18.15.0/24
description: syd1 production
site: syd1            # NetBox site slug (optional)
dhcp:                 # omit the block for a NetBox-only prefix
  enabled: true
  start: 200          # host-octet int
  stop: 220
router: 254           # host-octet int -> gateway IP
dns: [198.18.200.7]   # DHCP-scope DNS
next_server: 198.18.19.19
domain: main.unkin.net

Creates netbox_prefix (+ netbox_ip_range role dhcp + kea_subnet when dhcp set, + gateway netbox_ip_address). A prefix backing a network is tagged net:<name>.

Network schema (networks/<name>.yaml)

Per-network provisioning constants — hoisted here, never repeated per device.

subnet: net-198-18-15   # which subnet's prefix backs this network
gateway: 198.18.15.254
dns: [198.18.19.16]
search: main.unkin.net

Device schema (devices/<host>.yaml) — intent only

Declare only human intent. Serial, real interface names (which vary by model — OptiPlex 3060 = enp1s0, 3050/7080 = enp2s0), MACs and inventory are owned by the first-boot discovery image / PuppetDB, never hardcoded here.

site: syd1
role: roles::base                 # puppet mgmt_class / ENC role -> NetBox device role
model_hint: optiplex-3070         # NetBox device type; discovery confirms via dmidecode
provision:
  profile: almalinux9-dell_3070   # cobbler profile / kickstart template
  platform: almalinux9
networks:
  mgmt: 198.18.15.14/24           # requested IP; "" = next-available from the prefix
pxe: true
bootstrap_mac: a4:bb:6d:xx:xx:xx  # TRANSITIONAL — see below

Mapping: netbox_device (+ netbox_manufacturer/netbox_device_type/ netbox_device_role), netbox_ip_address (static) or netbox_available_ip_address (next-available, ignore_changes so a machine is never re-IPed). bootstrap_mac is transitional: it seeds one placeholder netbox_device_interface + netbox_mac_address so bootapi can key the PXE boot on MAC until the discovery image exists — remove it once discovery/backfill populates the real NICs.

Reality backfill (reality/<host>.yaml)

tools/backfill (Go) queries pdbmux — the PuppetDB multiplexer at https://pdbmux.k8s.syd1.au.unkin.net, which merges the old (Consul) and new (k8s) PuppetDBs and needs no auth — for already-provisioned hosts and emits their hardware reality as reviewable per-host YAML:

  • identity — serial, model, UUID (from dmi.product);
  • interfaces — every recordable NIC with MAC + CIDR IPs, including the overlay/loopback/kube-lb addresses Cobbler never had. Real NICs are always kept; virtual interfaces are kept only when they carry a routable address, which drops ephemeral Calico veths and lo while keeping flannel.1, kube-lb0, loopback*. Interface names are taken from Facter, never assumed;
  • inventory — CPU model/count, total RAM bytes, and physical disks (Ceph RBD and other virtual block devices are skipped).
make backfill                                   # all 13 existing physicals
make backfill BACKFILL_HOSTS="prodnxsr0001"     # a subset

Output is deterministic and idempotent (stable ordering, sorted lists), so re-running against unchanged facts leaves a clean diff. Unlike raw PuppetDB, pdbmux's HTTPS gateway is reachable from CI/workstations. The generator only writes committed config; netbox_device_interface/netbox_mac_address/ netbox_ip_address/netbox_inventory_item (plus device serial) reconcile it into NetBox on the next apply. NetBox stays authoritative for IP data.

Reality is wired into NetBox only for hosts that also have an intent devices/<host>.yaml; the 13 existing physicals get their intent seeded separately, so until then their reality/*.yaml is committed but inert.

Providers

Provider Source Notes
netbox e-breuninger/netbox 4.3.0 OpenTofu registry
kea artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/kea 0.0.1 cluster_ref=kea
vault hashicorp/vault 5.6.0 reads NetBox + Kea tokens

Tokens: Vault KV v2 kv/service/terraform/infra fields netbox_token + kea_token.

Blockers

  • NetBox not deployed yet (unmerged benvin/netbox argocd branch); NetBox sites must pre-exist. KeaAPI not deployed yet (argocd-apps PR #333). Seed kv/service/terraform/infra before the pipeline can auth.
S
Description
YAML-driven Terraform for NetBox IPAM, networks, and devices + Kea DHCP
Readme 86 KiB