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.
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.