{"slug": "building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab", "title": "Building UniFi Reactor: state-driven network automation for my Kubernetes homelab", "summary": "Robbe Verhelst built UniFi Reactor, a Kubernetes operator that watches UniFi Network state and turns it into declarative cluster automations. The operator polls the UniFi Network API, normalizes the state into keys, and reconciles Kubernetes Automation resources, enabling actions like scaling down ML workloads when the UPS is on battery or pausing downloads on backup WAN.", "body_md": "A few weeks ago I added a UPS and backup internet to my homelab. That solved one class of problem, but it made another one more obvious.\n\nMy UniFi gear knew when the WAN failed over. It knew when the UPS switched to battery. Kubernetes did not.\n\nSo if the main WAN failed at 3 AM, qBittorrent could keep happily seeding over a metered backup link. If power dropped, the UPS could be counting down its remaining runtime while the cluster continued doing background ML jobs, backups, and other work that absolutely did not need to happen during an outage.\n\nThat felt wrong. The network already had the state. The cluster just needed to react to it.\n\nThat is why I built [UniFi Reactor](https://github.com/robbeverhelst/unifi-reactor): a Kubernetes operator that watches UniFi Network state and turns it into declarative cluster automations.\n\nDocs are here: [reactor.robbeverhelst.com](https://reactor.robbeverhelst.com/)\n\nUniFi Reactor polls the UniFi Network API, normalizes what it sees into state keys, and reconciles Kubernetes `Automation`\n\nresources against those keys.\n\nExamples of state keys:\n\n`wan: primary`\n\nor `wan: backup`\n\n`internet: ok`\n\n, `degraded`\n\n, or `down`\n\n`ups: online`\n\nor `on-battery`\n\n`ups.battery: normal`\n\n, `low`\n\n, or `critical`\n\n`devices: all-online`\n\nor `degraded`\n\n`device.<name>: online`\n\nor `offline`\n\nThen you write rules like this:\n\n```\napiVersion: reactor.robbeverhelst.com/v1alpha1\nkind: Automation\nmetadata:\n  name: shed-ml-on-battery\n  namespace: reactor-system\nspec:\n  when:\n    provider: unifi\n    state:\n      ups: on-battery\n  actions:\n    - type: kubernetes.scale\n      target:\n        kind: Deployment\n        name: immich-machine-learning\n        namespace: immich\n      replicas: 0\n  onExit:\n    - type: kubernetes.scale\n      target:\n        kind: Deployment\n        name: immich-machine-learning\n        namespace: immich\n      replicas: 1\n```\n\nWhen the UPS goes on battery, Reactor scales down Immich machine learning. When mains power returns, it scales it back up.\n\nThat was the first real automation I deployed because it is boring in exactly the right way. Photo indexing is useful, but nobody cares if it pauses during a power cut.\n\nI already have Prometheus, Grafana, Gatus, and the usual homelab observability stack. They are good at telling me something happened.\n\nBut an alert that says \"the UPS is on battery\" is only half useful. The question I actually care about is: what should the system do while that condition is true?\n\nThat difference shaped the project:\n\nFor simple notification-only cases, Prometheus is probably still the better tool. Reactor becomes interesting when the response is operational: pause downloads, suspend CronJobs, scale down optional workloads, disable guest WiFi, or shed PoE load.\n\nThe first design choice was to make Reactor state-driven instead of event-driven.\n\nThat means polling is the source of truth. Webhooks can be a fast path later, but they should not be the mechanism of record.\n\nWhy? Because one-shot events are easy to miss. The controller can restart. The network can flap. A webhook can fail. If the system only reacts to edges, it can get stranded in the wrong mode.\n\nWith state reconciliation, the next observation corrects things.\n\nThe mental model is closer to Kubernetes itself: observe reality, compare it to desired state, reconcile.\n\nA simplified flow looks like this:\n\n```\nUniFi Network API\n      |\n      v\nUniFi provider observes hardware state\n      |\n      v\nNormalized keys: wan, ups, internet, devices, ...\n      |\n      v\nReactor matches Automation resources\n      |\n      v\nActions are applied while conditions hold\n```\n\nThe flagship use case is metered backup internet.\n\nI run a UniFi setup with a primary WAN and backup connectivity. Once failover is verified end-to-end, I want this kind of automation:\n\n```\napiVersion: reactor.robbeverhelst.com/v1alpha1\nkind: Automation\nmetadata:\n  name: pause-downloads-on-backup-wan\n  namespace: media\nspec:\n  when:\n    provider: unifi\n    state:\n      wan: backup\n  actions:\n    - type: kubernetes.scale\n      target:\n        kind: Deployment\n        name: qbittorrent\n        namespace: servarr\n      replicas: 0\n  onExit:\n    - type: kubernetes.scale\n      target:\n        kind: Deployment\n        name: qbittorrent\n        namespace: servarr\n      replicas: 1\n```\n\nWhen the network is on backup WAN, downloads stop. When primary WAN returns, they resume.\n\nThat is a tiny YAML file, but it encodes a very practical policy: do not burn backup data on background traffic.\n\nOther examples I want to add around this:\n\nSome of those are not implemented yet, but the model is clear.\n\nPower loss is the other obvious axis.\n\nA UPS buys time. The cluster should spend that time intelligently.\n\nThe first deployed reaction was:\n\n`ups: on-battery`\n\n-> scale `immich-machine-learning`\n\nfrom 1 to 0Future power-loss automations can become more aggressive as the battery drains:\n\n`ups: on-battery`\n\n-> suspend non-critical CronJobs`ups.battery: low`\n\n-> scale down heavier optional workloads`ups.battery: critical`\n\n-> shut down or isolate more aggressivelyOne important detail: `ups`\n\nand `ups.battery`\n\nare separate keys.\n\nThat is deliberate. If a single enum went from `online`\n\nto `on-battery`\n\nto `battery-low`\n\nto `critical`\n\n, an automation matching `on-battery`\n\nwould stop matching when the battery became low. That could accidentally fire `onExit`\n\nand scale workloads back up during the outage.\n\nSeparate keys avoid that. An automation can match `ups: on-battery`\n\nfor the whole outage, while another automation can additionally match `ups.battery: critical`\n\nfor escalation.\n\nA surprisingly important part of the design is `onExit`\n\n.\n\nReactor does not guess how to undo something. The automation says what should happen when the condition starts holding, and what should happen when it stops.\n\n```\nactions:\n  - type: kubernetes.scale\n    target:\n      kind: Deployment\n      name: qbittorrent\n      namespace: servarr\n    replicas: 0\n\nonExit:\n  - type: kubernetes.scale\n    target:\n      kind: Deployment\n      name: qbittorrent\n      namespace: servarr\n    replicas: 1\n```\n\nThat is more verbose than magic, but it is safer. Infrastructure automation gets scary when the tool assumes what \"back to normal\" means.\n\nThis was the part that made the project more than a wrapper around a few scripts.\n\nImagine qBittorrent should pause when:\n\nThose are independent reasons. Either one should keep qBittorrent down.\n\nIf one automation exits while the other still matches, Reactor must not scale the deployment back up.\n\nSo Reactor arbitrates shared targets. Desired-state actions like `kubernetes.scale`\n\nare treated as levels. The most restrictive active level wins. A workload comes back only when no matching automation still wants it down.\n\nThat avoids the classic failure mode where two scripts fight each other:\n\nReactor tracks the active claims instead.\n\nI wanted this to be safe enough to run in my actual cluster, not just impressive in a README.\n\nThe operator uses:\n\n`cluster-admin`\n\nFor example, actions like HTTP requests, UniFi WLAN changes, PoE cycling, and UPS outlet control are refused unless the destination or target is explicitly allowed.\n\nThat matters because a Kubernetes operator with access to your network controller is a sharp tool. It should not become \"curl with cluster permissions\".\n\nThe project already supports more than just scaling Deployments.\n\nAction families include:\n\n`kubernetes.scale`\n\n`kubernetes.cronjob.suspend`\n\n`kubernetes.cordon`\n\n`kubernetes.restart`\n\n`http.request`\n\n`notification.ntfy`\n\n, `notification.discord`\n\n, `notification.slack`\n\n`homeassistant.service`\n\n`qbittorrent.pause`\n\nand `qbittorrent.resume`\n\n`unifi.wlan.enable`\n\nand `unifi.wlan.disable`\n\n`unifi.poe.cycle`\n\n`unifi.outlet.cut`\n\nand `unifi.outlet.restore`\n\nThere is intentionally no arbitrary shell action. That would be convenient, and also how you turn a small operator into a haunted CI runner.\n\nA few lessons stood out.\n\nFirst, UniFi exposes useful operational state, but it is not always shaped the way you would design a clean public API. Normalizing it into a small vocabulary is worth the effort.\n\nSecond, \"event-driven\" sounds elegant until you care about recovery. For infrastructure state, reconciliation beats clever edge handling.\n\nThird, undo behavior deserves first-class design. It is easy to write automation that does something. It is harder to make it stop doing that thing at the right time.\n\nFourth, homelab projects are best when they are boring under pressure. A power cut is not the moment to discover your automation has opinions.\n\nThe project is public and released as a multi-arch container image and Helm chart.\n\n`v1.2.0`\n\nThe next things I want to harden are:\n\nThe broader idea is simple: your infrastructure already knows a lot about the conditions it is running under. UniFi Reactor is my attempt to let Kubernetes respond to that context without turning the homelab into a pile of one-off scripts.\n\nThat is the kind of boring automation I want more of.", "url": "https://wpnews.pro/news/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab", "canonical_source": "https://dev.to/robbeverhelst/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab-4k50", "published_at": "2026-08-16 10:17:42+00:00", "updated_at": "2026-08-16 10:41:53.499730+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["UniFi Reactor", "Robbe Verhelst", "Kubernetes", "UniFi", "Immich"], "alternates": {"html": "https://wpnews.pro/news/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab", "markdown": "https://wpnews.pro/news/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab.md", "text": "https://wpnews.pro/news/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab.txt", "jsonld": "https://wpnews.pro/news/building-unifi-reactor-state-driven-network-automation-for-my-kubernetes-homelab.jsonld"}}