The syd1 site does not exist, so the site data source failed the plan. Sites now follow the same declared-by-reference rule as device roles, types and manufacturers.
terraform-netbox
YAML-driven Terraform/Terragrunt for the state that belongs in NetBox: prefixes and IP ranges, logical networks, physical devices, platforms, and the custom-field definitions bootapi reads. NetBox is authoritative for all IP/interface data; this repo declares intent and lets discovery/PuppetDB fill in hardware reality.
Supersedes
terraform-infra, which also drove Kea DHCP. Kea is managed by the kea-operator CRs in argocd-apps (apps/base/dhcp-system/cr/) — both declaring the same subnets would fight, and two CRs for one prefix fails the kea config load. This repo is NetBox only.
Layout
config/<region>/<dc>/subnets/<name>.yaml # one file per subnet (NetBox prefix + pool range)
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>/custom-fields.yaml # custom-field DEFINITIONS
config/<region>/<dc>/managed-ips.yaml # extra manually-managed IPs
environments/<region>/<dc>/terragrunt.hcl # one Terragrunt env per region/dc
modules/netbox/ # the module
ci/puppetdb_backfill.py # reality backfill generator
make plan / make apply reconcile the YAML against NetBox. State lives in
Consul under infra/terraform/netbox/<region>/<dc>/state.
Sites, device roles, device types and manufacturers are all declared by being referenced: any slug a subnet or device names is created. A site that already exists in NetBox must be imported, not re-created, since site names are unique.
Subnet schema (subnets/<name>.yaml)
prefix: 198.18.15.0/24
description: syd1 production
site: syd1 # NetBox site slug (optional)
dhcp: # omit for a NetBox-only prefix
enabled: true
start: 200 # host-octet int
stop: 220
router: 254 # host-octet int -> gateway IP
Creates netbox_prefix (+ an netbox_ip_range with role dhcp and the gateway
netbox_ip_address). A prefix backing a network is tagged net:<name>.
The DHCP server config (pool, routers, DNS, next-server) is not here — it
is the KeaSubnet CR in argocd-apps. dhcp only records the pool in NetBox so
IPAM reflects what is handed out.
Device schema (devices/<host>.yaml) — intent only
Serial, real interface names (which vary by model) and inventory are owned by discovery/PuppetDB, never hardcoded here.
site: syd1
role: roles::base
model_hint: optiplex-3070 # NetBox device type AND bootapi catalog entry
provision:
platform: almalinux9 # NetBox platform slug
networks:
mgmt: 198.18.15.14/24 # requested IP; "" = next-available
pxe: true # device status staged vs active
bootstrap_mac: a4:bb:6d:xx:xx:xx # TRANSITIONAL
model_hint is written to the device's provision_template custom field: the
per-model OptiPlex catalog entries in bootapi-templates are named after the
device-type slug, so one field drives bootapi's per-model storage layout.
provision.platform becomes a netbox_platform and the device's platform_id,
which is bootapi's primary catalog selector.
bootstrap_mac seeds one placeholder interface + MAC so bootapi can key the PXE
boot on MAC until the discovery image exists; remove it once backfill populates
the real NICs.
Custom fields (custom-fields.yaml)
Field definitions for what bootapi reads: pxe_enabled,
provision_template, domain, nameservers, gateway. content_types are
NetBox app_label.model strings.
pxe_enabled's value is deliberately not managed here — it is runtime state.
bootapi's /provisioned callback clears it when an install finishes so the host
boots local disk instead of re-imaging; setting it from terraform would
re-enable installs on every apply.
PuppetDB reality backfill
ci/puppetdb_backfill.py sweeps provisioned hosts and emits their hardware
reality (serial/model/UUID + every interface's MAC/IPs) as reviewable YAML.
PuppetDB needs no auth but is not reachable from CI — run it in-cluster or
from puppet infra. Wiring its output into NetBox is a follow-up.
Providers + auth
| Provider | Source |
|---|---|
| netbox | e-breuninger/netbox 4.3.0 |
| vault | hashicorp/vault 5.6.0 |
The NetBox token is minted per run from the Vault netbox engine
(netbox/creds/terraform-netbox) and lease-revoked when the run ends; Consul
state creds come from consul_root/au/syd1/creds/terraform-netbox. Both are
granted to the terraform_netbox AppRole and the woodpecker_terraform_netbox
k8s role in terraform-vault.