The Idea

If you work with datacenters or network infrastructure, you probably know NetBox. It is the go to tool for documenting your infrastructure: sites, racks, devices, IP addresses, VLANs, circuits, the works. Most teams manage their NetBox data through the web UI or by calling the REST API directly. That works fine for small environments, but once you have multiple sites with hundreds of resources, clicking through forms gets old fast.

I have been thinking about this for a while. We already treat our Azure infrastructure as code with Bicep. Why not do the same for NetBox? Declare your sites, prefixes, VLANs in a Bicep file, run a deploy, and let the tooling figure out whether to create or update each resource. The bigger picture here is simplifying IP address management in Azure. If you can manage your NetBox IPAM data through Bicep, you can integrate it into the same deployment workflows you already use for your Azure resources. Reserve an IP range in NetBox and reference it when provisioning a virtual network, all in the same language. It also works the other way around. The repo includes a PowerShell script that queries Azure Resource Graph to discover your existing infrastructure (VNets, subnets, public IPs, VMs, regions) and generates a Bicep file that syncs all of it into NetBox. So you can both push infrastructure definitions to NetBox through Bicep deployments and pull your existing Azure state into NetBox automatically.

Inspiration

The thing that actually made me go from "this would be cool" to "I should try this" was discovering Anthony Martin's work on Bicep extensibility. Anthony is one of the people behind the Bicep compiler itself, and he built proof of concept extensions for GitHub and Azure Key Vault that show how you can use Bicep's local deploy feature to talk to any API, not just Azure Resource Manager.

Now, I am not a developer. I am an infrastructure person. I can read code well enough to understand what is going on, but sitting down and writing a .NET application from scratch is not something I would do on a weekend. So when I looked at Anthony's repos, my first thought was not "let me code this myself." It was: what if I feed his patterns and implementation to Claude and have it generate a Bicep extension for NetBox?

That turned out to work really well. Anthony's code is clean and well structured, which made it a great reference for Claude to follow. I could describe what I wanted, point it at the existing extensions as examples, explain how the NetBox API works, and iterate from there. The combination of a solid reference implementation and an AI that understands both the code patterns and the domain made this possible for someone who is not writing C# for a living.

Anthony's work is genuinely impressive and I recommend checking out his repos if you are interested in Bicep extensibility.

What the Extension Does

The extension currently supports 57 NetBox resource types across all the major API modules:

  • DCIM: Sites, regions, manufacturers, device types, devices, interfaces, racks, locations
  • IPAM: Prefixes, IP addresses, VLANs, VRFs, route targets, ASNs, aggregates
  • Virtualization: Clusters, virtual machines, VM interfaces, virtual disks
  • Circuits: Providers, circuit types, circuits
  • Tenancy: Tenants, tenant groups, contacts
  • VPN: Tunnels, IKE policies, IPSec profiles
  • Wireless: Wireless LANs, wireless links

Each resource type has a handler that knows how to look up existing resources, create new ones, and update existing ones. The deployments are fully idempotent. Run the same file twice and nothing changes. Run it after modifying a description and only that field gets patched.

How It Looks in Practice

Here is a simple example that sets up a datacenter with some basic networking:

targetScope = 'local'

@secure()
param netboxToken string
param netboxUrl string

extension netbox with {
  url: netboxUrl
  token: netboxToken
}

resource region 'Region' = {
  name: 'Sweden'
  description: 'Sweden'
}

resource site 'Site' = {
  name: 'Stockholm DC1'
  status: 'active'
  region: region.id
  description: 'Primary datacenter in Stockholm'
  physicalAddress: 'Sveavägen 1, Stockholm'
  timeZone: 'Europe/Stockholm'
}

resource manufacturer 'Manufacturer' = {
  name: 'Cisco'
  description: 'Cisco Systems'
}

resource roleRouter 'DeviceRole' = {
  name: 'Router'
  color: 'aa1409'
  description: 'Core and edge routers'
}

resource vlan 'VLAN' = {
  vid: 100
  name: 'Management'
  status: 'active'
}

resource prefix 'Prefix' = {
  prefix: '10.100.0.0/24'
  status: 'active'
  description: 'Management network'
}

resource gateway 'IPAddress' = {
  address: '10.100.0.1/24'
  status: 'active'
  dnsName: 'gw-stockholm-dc1.contoso.com'
  description: 'Default gateway'
}

Notice how we can use references within Bicep to assign relationships. The site gets its region by referencing region.id, just like you would link resources together in a regular Bicep deployment against Azure.

Deploying is a single command:

$env:NETBOX_URL = "https://netbox.contoso.com"
$env:NETBOX_TOKEN = "nbt_your-token-here"

bicep local-deploy main.bicepparam

And you get a nice summary table:

╭──────────────┬──────────┬───────────╮
│ Resource     │ Duration │ Status    │
├──────────────┼──────────┼───────────┤
│ region       │ 0,3s     │ Succeeded │
│ site         │ 0,4s     │ Succeeded │
│ manufacturer │ 0,3s     │ Succeeded │
│ roleRouter   │ 0,3s     │ Succeeded │
│ vlan         │ 0,4s     │ Succeeded │
│ prefix       │ 0,4s     │ Succeeded │
│ gateway      │ 0,5s     │ Succeeded │
╰──────────────┴──────────┴───────────╯

Azure Integration: From Cloud to NetBox

One thing I really wanted to show is how this fits into an Azure workflow. Say you deploy a public IP address in Azure and you want that IP tracked in NetBox automatically. Since the NetBox extension uses targetScope = 'local' and Azure deployments use targetScope = 'resourceGroup', you cannot mix them in a single Bicep file. But a simple two step workflow does the trick.

First, a regular Azure Bicep file that creates the public IP:

targetScope = 'resourceGroup'

param location string = resourceGroup().location

resource publicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = {
  name: 'pip-netbox-demo'
  location: location
  sku: {
    name: 'Standard'
  }
  properties: {
    publicIPAllocationMethod: 'Static'
  }
}

output ipAddress string = publicIp.properties.ipAddress

Then a NetBox Bicep file that registers that IP:

targetScope = 'local'

@secure()
param netboxToken string
param netboxUrl string
param azurePublicIp string

extension netbox with {
  url: netboxUrl
  token: netboxToken
}

resource ipAddress 'IPAddress' = {
  address: azurePublicIp
  status: 'active'
  description: 'Azure Public IP - registered via Bicep extension'
  dnsName: 'pip-netbox-demo.northeurope.cloudapp.azure.com'
}

Tie them together in a script:

# Step 1: Deploy to Azure and capture the IP
$ip = az deployment group create `
    --resource-group 'rg-netbox-demo' `
    --template-file './azure.bicep' `
    --query 'properties.outputs.ipAddress.value' `
    --output tsv

# Step 2: Register it in NetBox
$env:NETBOX_URL      = 'https://netbox.contoso.com'
$env:NETBOX_TOKEN    = 'nbt_your-token-here'
$env:AZURE_PUBLIC_IP = "$ip/32"

bicep local-deploy './netbox.bicepparam'

Deploy an Azure resource, grab the output, feed it into NetBox. Two commands and your source of truth is up to date. This is the kind of workflow that makes keeping NetBox in sync with your actual infrastructure realistic instead of something that falls behind after week one.

A note on scopes: you might be wondering why this requires two separate files and a script instead of just using Bicep modules. The reason is that targetScope = 'local' (used by extensions) and targetScope = 'resourceGroup' (used for Azure deployments) are completely different execution paths. Local deploy runs outside of ARM entirely, so you cannot mix Azure resources and extension resources in the same file, and you cannot call one scope from the other using modules. This is a Bicep platform limitation today. A CI/CD pipeline or a simple script is the natural place to tie the two steps together.

How It Works Under the Hood

The extension is a .NET 9 application that uses Microsoft's Azure.Bicep.Local.Extension SDK. Each of the 57 resource types gets a handler class. Most of them are surprisingly small because they inherit from a base class that handles all the HTTP logic, authentication, and create or update flow.

For example, the Site handler is literally three lines:

public class SiteHandler : SlugResourceHandler<Site>
{
    protected override string ApiPath => "/api/dcim/sites/";
}

The SlugResourceHandler base class takes care of looking up the resource by slug, auto generating the slug from the name if you do not provide one, and deciding whether to POST (create) or PATCH (update). The token authentication is also handled automatically. It detects whether you are using the older Token format or the newer Bearer format based on the nbt_ prefix.

Getting Started

You will need Bicep CLI v0.32+ with the experimental extensibility and local deploy features enabled. The setup is straightforward:

  1. Grab the binary for your platform from the GitHub releases page (or build from source if you prefer)
  2. Package it as a Bicep extension using bicep publish-extension
  3. Point your bicepconfig.json at the packaged extension
  4. Write your Bicep files and deploy with bicep local-deploy

The repository has sample templates for different scenarios: a basic single site setup, a full datacenter with multiple regions, IPAM configurations, virtualization clusters, and more.

What is Next

There are a few things I want to tackle next:

  • More resource types. NetBox has a massive API surface and we are covering the most common ones, but there is more to add.
  • Delete support. Right now the extension only creates and updates. Removing resources still needs to happen through the NetBox UI or API directly.
  • Array properties. The Bicep extensibility SDK does not support array types yet, which means we cannot set things like tag lists on resources. This is a limitation on the SDK side that will hopefully be resolved in a future Bicep release.

If you are managing NetBox infrastructure and already familiar with Bicep, give it a try. The repo is open source and contributions are welcome.

//Sebastian