Update UniFi Pulumi plan: switch to ubiquiti-community provider (#187)
## Summary - Switch provider from filipowm/unifi (inactive maintainer, showstopper bug #94 wiping firewall rules) to ubiquiti-community/unifi (actively maintained, API key auth) - Add UX7 config backup prerequisite before adopting IaC - Fix safety guard: check default route interface instead of hostname (runs from gilbert, not indri) - Update 1Password paths to match actual item (`op://blumeops/unifi/credential`) - Fix ringtail references: not a Raspberry Pi, stays on WiFi (removed from wired topology) - Update doc steps for already-existing reference files ## Test plan - [x] Pre-commit hooks pass - [x] `docs-check-links` pass - [x] `docs-check-index` pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/187
This commit is contained in:
parent
b77ae19f20
commit
49ec05041c
3 changed files with 50 additions and 36 deletions
1
docs/changelog.d/update-unifi-pulumi-plan.infra.md
Normal file
1
docs/changelog.d/update-unifi-pulumi-plan.infra.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Updated UniFi Pulumi plan: switched provider to ubiquiti-community/unifi, added config backup step, fixed safety guard and 1Password paths.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "Plan: Add UniFi Pulumi Stack"
|
||||
modified: 2026-02-11
|
||||
modified: 2026-02-13
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
|
|
@ -11,7 +11,7 @@ tags:
|
|||
# Plan: Add UniFi Pulumi Stack
|
||||
|
||||
> **Status:** Planned (not yet executed)
|
||||
> **Blocked by:** Ethernet switch purchase and cabling
|
||||
> **Blocked by:** 1Password credential setup (API key)
|
||||
|
||||
## Background
|
||||
|
||||
|
|
@ -24,13 +24,13 @@ The UniFi Express 7 (UX7) is the home WiFi router, currently unmanaged. This pla
|
|||
- **Consistency** — joins the existing Pulumi stacks for Tailscale ACLs and DNS
|
||||
- **Network segmentation** — declare main/guest/IoT WiFi networks with proper firewall zones
|
||||
|
||||
### Why This Is Blocked
|
||||
### Ethernet Requirement (Resolved)
|
||||
|
||||
The UX7 has one LAN port, currently connected to [[sifaka]]. Modifying WiFi settings over WiFi would sever the management connection mid-apply. We need:
|
||||
The UX7 has one LAN port. Modifying WiFi settings over WiFi would sever the management connection mid-apply. This was resolved by installing:
|
||||
|
||||
1. **Two switches** (UniFi Switch Flex Mini recommended) daisy-chained:
|
||||
- Switch A by the router: connects UX7, sifaka
|
||||
- Switch B on the desk (~12ft cable): connects indri, ringtail, and optionally gilbert
|
||||
- Switch B on the desk (~12ft cable): connects indri and gilbert
|
||||
2. **Cat6 Ethernet cables**: one ~12ft run between switches, plus short cables for each device
|
||||
|
||||
```
|
||||
|
|
@ -39,8 +39,7 @@ UniFi Express 7 [LAN port]
|
|||
├── sifaka (short cable)
|
||||
└── ~12ft Cat6 ──→ Switch B (on desk)
|
||||
├── indri (Cat6)
|
||||
├── ringtail (Cat6)
|
||||
└── (gilbert via USB-C adapter, optional)
|
||||
└── gilbert (USB-C adapter)
|
||||
```
|
||||
|
||||
Daisy-chaining is standard Layer 2 networking — no speed loss per device, no subnet impact. The only shared bottleneck is the 1 Gbps uplink between the two switches, which is more than adequate for homelab use. UniFi Flex Minis will appear in the UX7's controller for monitoring and eventual Pulumi management.
|
||||
|
|
@ -52,25 +51,38 @@ Before starting the execution session:
|
|||
- [ ] Purchase 2x UniFi Switch Flex Mini (USW-Flex-Mini)
|
||||
- [ ] Purchase Cat6 cables: 1x ~12ft, 3-4x short (~3ft)
|
||||
- [ ] Cable everything up and verify all devices have network connectivity
|
||||
- [ ] Verify indri has an active wired Ethernet connection: `networksetup -getinfo "Ethernet"` should show an IP address
|
||||
- [ ] Verify the machine running Pulumi (gilbert) has an active wired Ethernet connection as its default route: `route -n get default` should show a non-Wi-Fi interface
|
||||
- [ ] **Back up the UX7 configuration** via `https://192.168.1.1` → Settings → System → Backup, and download a `.unf` backup file. Store it safely before making any IaC changes. This provides a rollback path if a provider bug corrupts network or firewall state.
|
||||
- [ ] Create an API key on the UX7 via `https://192.168.1.1` → Settings → Control Plane → API (preferred over username/password for IaC)
|
||||
- [ ] Store UniFi credentials in 1Password: vault `blumeops`, item `unifi - blumeops`, fields `api_key` and `url`
|
||||
- [ ] Store UniFi API key in 1Password: vault `blumeops`, item `unifi`, category `API_CREDENTIAL`
|
||||
- [ ] Verify Pulumi CLI version is >= v3.147.0 (`pulumi version`)
|
||||
|
||||
## Provider: filipowm/unifi via `pulumi package add`
|
||||
## Provider: ubiquiti-community/unifi via `pulumi package add`
|
||||
|
||||
We use `pulumi package add terraform-provider filipowm/unifi 1.0.0` to consume the [filipowm fork](https://github.com/filipowm/terraform-provider-unifi) of the UniFi Terraform provider directly from Pulumi. This approach:
|
||||
We use `pulumi package add terraform-provider ubiquiti-community/unifi` to consume the [ubiquiti-community fork](https://github.com/ubiquiti-community/terraform-provider-unifi) of the UniFi Terraform provider directly from Pulumi. This approach:
|
||||
|
||||
- **Generates a local Python SDK** in `./sdks/unifi/` and adds a package reference to `Pulumi.yaml`
|
||||
- **Supports zone-based firewalls** — needed for main/guest/IoT WiFi segmentation and a dedicated blumeops services subnet
|
||||
- **Supports API key authentication** — cleaner than username/password for IaC
|
||||
- **Actively maintained** — v1.0.0, 520+ commits, not the abandoned upstream
|
||||
- **Actively maintained** — v0.41.12 (Jan 2026), responsive maintainer, 12 releases since Oct 2025
|
||||
- **Uses Pulumi Cloud state** — same as existing stacks, free tier
|
||||
- **No fork/bridge maintenance** — just re-run `pulumi package add` on new provider versions
|
||||
- **Broader ecosystem** — part of the [ubiquiti-community](https://github.com/ubiquiti-community) org alongside go-unifi, unifi-api, and other tools
|
||||
|
||||
### Why Not pulumiverse_unifi?
|
||||
### Why Not Other Providers?
|
||||
|
||||
The `pulumiverse_unifi` PyPI package bridges from `paultyng/terraform-provider-unifi`, which is in maintenance mode. It lacks zone-based firewalls, API key auth, and many newer resource types. The filipowm fork is the active community successor.
|
||||
| Provider | Why Not |
|
||||
|----------|---------|
|
||||
| `pulumiverse_unifi` | Bridges `paultyng/terraform-provider-unifi`, which is abandoned. No API key auth, no newer resource types. |
|
||||
| `filipowm/unifi` | Maintainer unresponsive since April 2025. Critical bug ([#94](https://github.com/filipowm/terraform-provider-unifi/issues/94)): applying `unifi_network` resources wipes all zone-based firewall rules. Unmerged community fix PRs. |
|
||||
| `paultyng/unifi` | Abandoned since March 2023. No API key auth, no zone-based firewall. |
|
||||
|
||||
### Zone-Based Firewall: Deferred
|
||||
|
||||
The ubiquiti-community provider does not yet support zone-based firewall resources ([#77](https://github.com/ubiquiti-community/terraform-provider-unifi/issues/77)). Zone-based firewall rules will be managed manually in the UX7 web UI until provider support lands. This is acceptable because:
|
||||
|
||||
- The initial goal is bringing networks, WLANs, and DHCP under IaC
|
||||
- Network segmentation (which needs firewall zones) is a future phase
|
||||
- The filipowm provider — the only one with zone firewall support — has a showstopper bug that makes it unusable for this purpose anyway
|
||||
|
||||
## Network Segmentation Goals
|
||||
|
||||
|
|
@ -124,7 +136,7 @@ Following the conventions of `pulumi/tailscale/` and `pulumi/gandi/`:
|
|||
```
|
||||
pulumi/unifi/
|
||||
├── Pulumi.yaml # name: blumeops-unifi, python runtime, uv toolchain
|
||||
│ # includes parameterized package reference for filipowm/unifi
|
||||
│ # includes parameterized package reference for ubiquiti-community/unifi
|
||||
├── Pulumi.home-network.yaml # Stack config: router_url, site
|
||||
├── pyproject.toml # Python >=3.11, pulumi>=3.0.0
|
||||
├── sdks/unifi/ # Generated Python SDK from pulumi package add
|
||||
|
|
@ -140,18 +152,21 @@ pulumi/unifi/
|
|||
|
||||
| Variable | Value | Notes |
|
||||
|----------|-------|-------|
|
||||
| `UNIFI_API_KEY` | from 1Password | API key created in UX7 control plane |
|
||||
| `UNIFI_API` | `https://192.168.1.1:443` | No `/api` suffix — SDK auto-discovers `/proxy/network` for UniFi OS |
|
||||
| `UNIFI_API_KEY` | `op read "op://blumeops/unifi/credential"` | API key created in UX7 control plane |
|
||||
| `UNIFI_API` | `https://192.168.1.1` | No `/api` suffix — SDK auto-discovers `/proxy/network` for UniFi OS |
|
||||
| `UNIFI_INSECURE` | `true` | UX7 uses a self-signed TLS certificate |
|
||||
|
||||
### Safety Guard
|
||||
|
||||
The `__main__.py` must fail fast before creating any Pulumi resources if:
|
||||
The `__main__.py` must fail fast before creating any Pulumi resources if the default network route goes through Wi-Fi. This prevents accidentally modifying WiFi settings while connected over WiFi (which would sever the management connection mid-apply).
|
||||
|
||||
1. **Wrong host** — `platform.node()` is not `indri`
|
||||
2. **No wired connection** — `networksetup -getinfo "Ethernet"` shows no active Ethernet
|
||||
The check works as follows:
|
||||
|
||||
This prevents accidentally running the stack from gilbert over WiFi.
|
||||
1. Run `route -n get default` and extract the `interface:` field (e.g., `en5`)
|
||||
2. Run `networksetup -listallhardwareports` and find which hardware port owns that interface
|
||||
3. If the hardware port is `Wi-Fi`, abort with an error
|
||||
|
||||
This is host-agnostic — it works on both gilbert (where the Ethernet adapter is `AX88179A` on `en5`) and indri (where it's `Ethernet` on `en0`).
|
||||
|
||||
## Execution Steps
|
||||
|
||||
|
|
@ -161,7 +176,7 @@ This prevents accidentally running the stack from gilbert over WiFi.
|
|||
mkdir -p pulumi/unifi
|
||||
cd pulumi/unifi
|
||||
# Create Pulumi.yaml, pyproject.toml, .gitignore, __main__.py
|
||||
pulumi package add terraform-provider filipowm/unifi 1.0.0
|
||||
pulumi package add terraform-provider ubiquiti-community/unifi
|
||||
# This generates sdks/unifi/ and updates Pulumi.yaml with package reference
|
||||
pulumi install
|
||||
uv sync
|
||||
|
|
@ -171,7 +186,7 @@ uv sync
|
|||
|
||||
Create `Pulumi.yaml`, `Pulumi.home-network.yaml`, `pyproject.toml`, `.gitignore`, and `__main__.py`. The main program should declare:
|
||||
|
||||
- **Ethernet safety guard** (hostname + wired connection check)
|
||||
- **Ethernet safety guard** (verify default route is not Wi-Fi)
|
||||
- **Default LAN network** resource (corporate, `192.168.1.0/24`, DHCP)
|
||||
- **WiFi WLAN** resources (commented out initially — need SSID names and IDs from the controller)
|
||||
- **Exports** for router IP, network ID, subnet
|
||||
|
|
@ -181,12 +196,12 @@ Create `Pulumi.yaml`, `Pulumi.home-network.yaml`, `pyproject.toml`, `.gitignore`
|
|||
Create `mise-tasks/unifi-preview` and `mise-tasks/unifi-up` following the pattern from `tailnet-up`/`dns-up`:
|
||||
|
||||
```bash
|
||||
UNIFI_API_KEY=$(op read "op://blumeops/unifi - blumeops/api_key")
|
||||
export UNIFI_API="https://192.168.1.1:443"
|
||||
UNIFI_API_KEY=$(op read "op://blumeops/unifi/credential")
|
||||
export UNIFI_API="https://192.168.1.1"
|
||||
export UNIFI_INSECURE="true"
|
||||
```
|
||||
|
||||
### Step 4: Initialize Stack (on indri)
|
||||
### Step 4: Initialize Stack
|
||||
|
||||
```fish
|
||||
cd pulumi/unifi
|
||||
|
|
@ -210,9 +225,7 @@ Adjust `__main__.py` resource properties to match the actual controller state un
|
|||
|
||||
### Step 6: Documentation Updates
|
||||
|
||||
- Create `docs/reference/infrastructure/unifi.md` reference card
|
||||
- Update `docs/reference/infrastructure/hosts.md` — link UniFi row to `[[unifi|Details]]`
|
||||
- Update `docs/reference/reference.md` — add `[[unifi]]` to Infrastructure section
|
||||
- Update `docs/reference/infrastructure/unifi.md` — remove `(planned)` markers, update provider to ubiquiti-community
|
||||
- Add changelog fragment
|
||||
|
||||
### Step 7: Verify
|
||||
|
|
@ -223,15 +236,16 @@ Adjust `__main__.py` resource properties to match the actual controller state un
|
|||
|
||||
## Known Limitations
|
||||
|
||||
- **macOS-specific guard** — the `networksetup` check only works on macOS, which is fine since indri is permanently macOS
|
||||
- **macOS-specific guard** — the `networksetup` and `route` checks only work on macOS, which is fine since the stack is run from gilbert or indri, both permanently macOS
|
||||
- **User group ID discovery** — the provider may not expose a `get_user_group` data source. Must be discovered manually from the controller API (`/proxy/network/api/s/default/rest/usergroup`) and hardcoded
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **Firewall rules** — declare zone-based firewall rules after the initial import is stable
|
||||
- **Zone-based firewall rules** — manage via Pulumi once ubiquiti-community adds support ([#77](https://github.com/ubiquiti-community/terraform-provider-unifi/issues/77)). Until then, configure manually in the UX7 web UI.
|
||||
- **Network segmentation** — depends on zone-based firewall support; see goals above
|
||||
- **UnPoller** — add Prometheus metrics exporter for UniFi gear, integrates with existing Grafana stack
|
||||
- **Switch management** — manage the USW-Flex-Minis via the same Pulumi stack once adopted into the UX7 controller
|
||||
- **Provider updates** — re-run `pulumi package add terraform-provider filipowm/unifi <new-version>` to update
|
||||
- **Provider updates** — re-run `pulumi package add terraform-provider ubiquiti-community/unifi` to update
|
||||
|
||||
## Reference Pattern Files
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ ISP Modem
|
|||
├── sifaka (Synology NAS)
|
||||
└── ~12ft Cat6 ──→ Switch B (on desk)
|
||||
├── indri (Mac Mini, primary server)
|
||||
├── ringtail (Raspberry Pi)
|
||||
└── (gilbert via USB-C adapter, optional)
|
||||
└── gilbert (USB-C adapter)
|
||||
```
|
||||
|
||||
All wired devices share the `192.168.1.0/24` subnet. The two daisy-chained UniFi Switch Flex Minis provide enough ports for all devices while using the UX7's single LAN port.
|
||||
|
|
@ -67,7 +66,7 @@ See [[add-unifi-pulumi-stack]] for the full implementation plan.
|
|||
|
||||
## Authentication
|
||||
|
||||
The provider uses an API key created in the UX7 control plane (Settings → Control Plane → API). The key is stored in 1Password (`op://blumeops/unifi - blumeops/api_key`) and injected via mise task environment variables.
|
||||
The provider uses an API key created in the UX7 control plane (Settings → Control Plane → API). The key is stored in 1Password (`op://blumeops/unifi/credential`) and injected via mise task environment variables.
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue