Why I Wrote a New Terraform Provider for UniFi
I have been doing more work around local UniFi management lately, and one thing kept bothering me: there was not a Terraform workflow for UniFi that I actually wanted to use.
(Yes, there are several other UniFi providers out there - but they all basically forked the O.G. provider: https://github.com/paultyng/terraform-provider-unifi, and provide a small amount of additional feature support on top of that [now archived] provider. Moving on.)
UniFi is good when you are clicking around in one controller. It gets less good when you want repeatability. Networks, WiFi broadcasts, firewall rules, DHCP reservations, ACLs. That all turns into remembered UI state unless you put some structure around it.
So I wrote one.
The result is badgerops/unifi, a Terraform provider for UniFi Network configuration based on the actual UniFi OpenAPI Spec. It is published on the Terraform Registry, and people are already using it. As of May 20, 2026, there's been 153 downloads. That is still early, but it is enough to confirm that this was not just a problem I had.
The problem I was actually trying to solve
As a recap, I did not start this because I wanted to write a provider for the sake of writing a provider.
I started because, like any self-respecting Sysadmin, I wanted UniFi configuration to behave more like infrastructure and less like a pile of clicks. I wanted to define site state in code, review changes in a normal workflow, and stop relying on memory and screenshots to understand how a controller was configured.
I spent time looking at the existing UniFi Terraform options. The short version is not that anybody did something wrong. The original provider was based off go-unifi which was built by decompiling the original Unifi Network application Jar file, and generating code from the json files contained in that Jar. All of the other providers fork and extend on that same basic premise.
Sometime in recent history, Ubiquiti started shipping an OpenAPI spec file inside their Network application code, so I thought: “hey, let’s use that!”
I fired up my trusty friend claude and started planning the implementation, giving the existing configuration of my UniFi network, the OpenAPI spec, and a simple instruction: "Go make me a Terraform provider"
A few hours later - (and much back and forth, redirecting, hand-editing, and mild cussing) I had a functioning provider that showed 0 drift between the previous (paultyng provider) state and the current plan with my own shiny provider.
On to the nitty gritty!
What this provider is
This provider targets core UniFi Network configuration. This includes things like:
- networks
- WiFi broadcasts
- firewall zones and policies
- firewall policy ordering
- traffic matching lists
- DNS policies
- ACL rules and ACL rule ordering
- DHCP reservations
It also includes data sources for the lookup and reference objects you need to build useful configurations: sites, devices, networks, WiFi broadcasts, firewall zones and policies, VPN references, DPI applications, countries, RADIUS profiles, device tags, WANs, switch stacks, MC-LAG domains, and LAGs.
It is intentionally aimed at durable configuration state. It is not trying to be a giant wrapper around every controller action. I am much more interested in the parts of UniFi that benefit from reviewable, repeatable Terraform configuration than in one-off operational actions. (Those should still be clicks, imho)
Why I built it around the OpenAPI snapshot
A big part of the design is that the repository tracks a committed UniFi Network OpenAPI snapshot and generates the low-level client code from that.
That matters because the UniFi API story is useful, but not perfectly clean. If you want a provider to stay understandable over time, it helps a lot to anchor it to a real contract instead of a bunch of “the controller seemed to accept this last time I tried it” logic.
So the provider is shaped around the committed snapshot, and the generated client stays behind explicit translation code. That means the Terraform surface stays intentional. Fields exist because they are supported and mapped on purpose, not because raw JSON happened to leak through.
The current public release, 0.2.12, refreshed that snapshot from UniFi Network 10.2.105 to 10.3.58. That brought in support for newer WiFi schema additions, including open security encryption modes and standard-broadcast DNS assistance configuration.
This is also part of why I spent time on the gitops tooling around the provider. There is weekly automation to watch upstream UniFi package releases and flag when the committed snapshot likely needs a refresh. That does not magically make maintenance happen, but it does mean staying current is built into the project shape instead of being a vague future intention.
The API reality
In general, if UniFi (or any other service provider) exposes something in the integration API, that is the surface I want to build against.
I did not want to turn this into Terraform over random private controller endpoints.
But. DHCP reservations are useful enough - and I needed them - that I made one explicit exception instead of pretending the official surface already covered them. (Because they didn't)
The provider is integration-API-first, with one narrow legacy exception for unifi_dhcp_reservation. The reason is the current committed OpenAPI spec does not expose DHCP reservation writes. For that one resource, the provider uses the legacy local Network client database endpoint. Fingers crossed that this changes soon.
Even there, I still tried to keep the behavior narrow and explicit. The provider is not “private API everywhere.” It is a small, documented exception where the official surface is not there yet.
What it covers today
Today, the provider gives you a usable base for managing real UniFi site configuration with Terraform. It has generated docs, checked-in examples, Registry-ready release packaging, and tests around the controller behaviors that matter for these resources.
I also spent time chasing firewall behavior against real controller quirks which consumed... time... That took a while to sort out and figure a good way to report if there is a controller issue. Still trying to figure that out, but for now we just have a bunch of unit tests and log output.
The fact that it is already past its first hundred downloads is a useful sanity check. It does not make the provider mature overnight, but it does suggest there are other people who want this same shape of workflow.
A small example
This is the kind of configuration I wanted to be able to write:
terraform {
required_providers {
unifi = {
source = "badgerops/unifi"
version = "0.2.12"
}
}
}
provider "unifi" {
api_url = var.unifi_api_url
api_key = var.unifi_api_key
allow_insecure = false
}
data "unifi_site" "main" {
name = "default"
}
resource "unifi_network" "management" {
site_id = data.unifi_site.main.id
name = "management"
management = "GATEWAY"
vlan_id = 200
ipv4_configuration = {
host_ip_address = "10.200.0.1"
prefix_length = 24
dhcp = {
mode = "SERVER"
range = {
start = "10.200.0.10"
stop = "10.200.0.200"
}
}
}
}
resource "unifi_dhcp_reservation" "switch" {
site_id = data.unifi_site.main.id
mac_address = "AA:BB:CC:DD:EE:01"
fixed_ip = "10.200.0.25"
}That is much closer to the workflow I wanted: declare the shape of the site, review the change, apply it, and keep going.
Sharp edges, because UniFi is still UniFi
I do not think a post like this is useful if it pretends the platform is cleaner than it is.
There are a few important caveats:
- Zone-based firewall needs to be enabled in UniFi before the firewall resources work.
- Controller behavior around firewall rules can be quirky, especially with some combinations of filters and action settings.
- DHCP reservations still use a narrow legacy API exception because the integration API does not expose reservation writes.
- The provider is for configuration workflows, not controller operations like adoption, telemetry, or device lifecycle management.
Why this project matters to me
This scratches a very real operational itch for me.
It is useful in my homelab. It is potentially useful in client work. It is also a good exercise in building something carefully around an API surface that is useful, even if imperfect.
I enjoyed building something that actually solved a problem for people-other-than-me, and, it was good to put my AI friend to work on something other than a task manager (heh)
What is next
The short list from here is pretty straightforward:
- keep expanding resource coverage where the integration API makes sense for Terraform
- keep testing against real controller behavior instead of assuming the docs tell the whole story (Anyone got a large deployment and want to play?)
- reduce surprises around UniFi firewall behavior
- move DHCP reservations back to the official API if UniFi exposes that path later
- keep the provider aligned with future upstream snapshot changes
The weekly upstream check is part of that maintenance story. It is the best way I could think of that keeps this project from quietly drifting out of date.
Give it a try
If you use UniFi and Terraform, give it a try.
If it breaks in an interesting way, definitely tell me.
-BadgerOps
GitHub: https://github.com/BadgerOps/terraform-provider-unifi
Terraform Registry: https://registry.terraform.io/providers/BadgerOps/unifi/latest