unkin-agent be53170557
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Derive the site from the config path and split out global objects
Stop repeating the site slug inside the YAML: terragrunt passes the dc as
site_slug and site.yaml carries only site-wide attributes. Declare device
types, roles, platforms, custom fields and the dhcp IPAM role once in
config/global via their own unit and state; the per-site unit looks them up
by slug or name.
2026-10-03 18:34:40 +10:00

terraform-netbox

YAML-driven Terraform/Terragrunt for the NetBox catalogue: the stable reference data a host is planned against. Device records are not here.

Ownership split

Owner Objects
this repo sites, prefixes, DHCP pool ranges, gateways, managed IPs, VLAN groups + VLANs, device types + port templates, manufacturers, device roles, platforms, custom-field definitions, network profiles, network equipment (switch device records)
autodiscovery agent host device records, host interfaces, MAC addresses, switch cabling
UI host names, role assignment, IP allocation (inventory -> planned)

Writing a host device from here would fight both of them, so there is no host netbox_device. Switches are the exception: nothing probes one, and its ports must exist before discovery can cable a host interface to them.

Supersedes terraform-infra, which also drove Kea DHCP. Kea is managed by the kea-operator CRs in argocd-apps (apps/base/dhcp-system/cr/).

Conventions

A per-site config path is config/<region>/<dc>/, and <dc> is the NetBox site slug: terragrunt derives it from the directory and passes it in, so the site is never named again inside the YAML.

Device types, manufacturers, device roles, platforms, custom-field definitions and the dhcp IPAM role are global objects in NetBox, not site-scoped. They are declared once in config/global/ by their own terragrunt unit and state; per-site units look them up by slug or name. Two per-site units creating them would collide on name as soon as a second region/dc exists.

Layout

config/global/device-types/<slug>.yaml                   # manufacturer, model, port templates
config/global/device-roles.yaml
config/global/platforms.yaml                             # slug -> display name
config/global/custom-fields.yaml                         # custom-field DEFINITIONS
config/<region>/<dc>/site.yaml                           # site-wide attributes; slug is <dc>
config/<region>/<dc>/subnets/net-<a>-<b>-<c>-<d>-<len>.yaml
config/<region>/<dc>/vlans.yaml
config/<region>/<dc>/network-profiles/<name>.yaml
config/<region>/<dc>/network-devices.yaml                # switches, keyed by device name
config/<region>/<dc>/managed-ips.yaml
environments/global/terragrunt.hcl
environments/<region>/<dc>/terragrunt.hcl
modules/netbox-global/
modules/netbox/

make plan / make apply reconcile the YAML against NetBox across both units. State lives in Consul under infra/terraform/netbox/global/state and infra/terraform/netbox/<region>/<dc>/state. A site that already exists in NetBox must be imported, not re-created: site names are unique.

Site (site.yaml)

Site-wide attributes only. The NetBox site name defaults to the slug; set name only if the display name must differ from it.

description: Sydney region, datacentre 1

Subnets (subnets/net-<a>-<b>-<c>-<d>-<len>.yaml)

The filename carries the network and mask length; the human label lives in description.

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

Creates netbox_prefix, plus a netbox_ip_range with role dhcp and the gateway netbox_ip_address. dns, domain and next_server are read by no resource today; they are retained so a later change can drive Kea from this repo without re-deriving them.

Device types (config/global/device-types/<slug>.yaml)

ports declares interface templates as ranges, not 28 hand-written entries. NetBox instantiates them on every device created from the type.

manufacturer: TP-Link
model: Omada SG3428X-M2
u_height: 1
ports:
  - name_format: two-gigabitEthernet 1/0/%d   # single %d
    type: 2.5gbase-t
    first: 1
    count: 24
  - name_format: ten-gigabitEthernet 1/0/%d
    type: 10gbase-x-sfpp
    first: 25
    count: 4

Network devices (network-devices.yaml)

Switches only; host records come from autodiscovery. device_type and role name global objects, looked up by slug/name. chassis_mac is written to the custom field discovery matches an LLDP neighbour on.

sg3429x-m2-01:
  device_type: sg3428x-m2   # config/global/device-types/ key
  role: network-switch      # config/global/device-roles.yaml key
  serial: 225C578000245     # optional
  chassis_mac: b8:fb:b3:a7:fa:24   # optional

VLANs (vlans.yaml)

The site gets its own site-scoped netbox_vlan_group, so the same VID may exist in more than one site.

vid_ranges: [[1, 4094]]
vlans:
  - vid: 101
    name: fabric_common_01

Network profiles (network-profiles/<name>.yaml)

A profile describes the interfaces a logical host should have. It is published as a netbox_config_context scoped to its site (and roles, if given), so the UI reads profiles from the NetBox API rather than from git.

description: Dual-homed fabric host
weight: 1000
interfaces:
  - name: fabric0
    vlan: 101                       # VID in the site's VLAN group
    subnet: net-198-18-13-0-24      # subnet key; the prefix is resolved into the context
    subinterfaces: []               # optional, same shape

Custom fields (config/global/custom-fields.yaml)

Field definitions for what bootapi reads: pxe_enabled, provision_template, domain, nameservers, gateway; plus chassis_mac, which autodiscovery matches an LLDP neighbour on. content_types are NetBox app_label.model strings. pxe_enabled's value is deliberately not managed here — it is runtime state cleared by bootapi's /provisioned callback.

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.

S
Description
YAML-driven Terraform for NetBox: prefixes, IPs, devices, platforms and custom fields
Readme 102 KiB