unkinben 66ae5f5f3c
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Point HA peer URLs at per-pod ClusterIP Services
## Why
kea-dhcp4 crash-loops at HA hook load: kea 2.6's HA hook parses each peer url host as an IP literal and never resolves DNS, so the StatefulSet headless hostnames are rejected ("Failed to convert string to address ..."). Verified in-cluster that only an IP works (short name, FQDN both fail; `kea-dhcp4 -t` does not exercise this, which is why the v0.1.3 wait did not catch it). Pod IPs cannot be baked into the config because they change on restart and would roll-loop the StatefulSet via the config hash.

## How
- create one ClusterIP Service per HA peer, selecting the pod by its statefulset.kubernetes.io/pod-name label, with publishNotReadyAddresses so peers are routable during bootstrap
- render each HA peer url as its peer Service ClusterIP (a stable IP literal, safe in the config hash); reconcile Services before the ConfigMap and requeue until the ClusterIPs are allocated
2026-08-09 18:59:20 +10:00

kea-operator

A Kubernetes operator that runs ISC Kea DHCPv4 servers to replace the legacy ISC dhcpd VM used for PXE-booting physical hosts. Modelled on the sibling bind-operator.

Namespace: dhcp-system. API group: kea.unkin.net/v1alpha1.

Custom resources

Kind Purpose
KeaCluster Spawns a StatefulSet of kea-dhcp4 + kea-ctrl-agent pods, an HA control channel, and an anycast DHCP Service. Holds global config (domain, lease times, NTP, PXE option-defs).
KeaSubnet One DHCPv4 subnet referenced to a KeaCluster (clusterRef). CIDR, optional pools, routers, DNS, domain, next-server. Pool-less subnets are declared so relayed requests are still serviced.
KeaClientClass PXE boot class matched on client architecture (option 93), e.g. legacy/undionly.kpxe, uefi-64/ipxe.efi.
KeaAPI Spawns the Terraform-friendly REST API service (see below).

The KeaCluster controller lists the matching subnets and client classes, renders a deterministic kea-dhcp4.conf (+ kea-ctrl-agent.conf) into a ConfigMap, and rolls the StatefulSet via a config-hash annotation stamped on the pod template (identical trick to bind-operator). Ready pods are additionally hot-reloaded best-effort via the kea-ctrl-agent REST control channel (config-reload), analogous to rndc reconfig.

HA design

Mode: hot-standby, memfile lease DB (non-persistent). PXE leases are ephemeral, low-volume, and drawn from tiny pools (.200.220). Hot-standby keeps a single active primary answering all DHCP with a warm secondary that syncs leases over the HA control channel and auto-promotes on primary failure — avoiding the split-pool double-allocation two uncoordinated servers would cause. Load-balancing mode is also selectable via spec.ha.mode.

Kea HA needs a stable per-peer identity (each server must know which peer it is, and peers reference each other by stable URL). That is exactly why this operator (like bind-operator) uses a StatefulSet rather than a bare Deployment: pods get stable ordinals (<cluster>-0, <cluster>-1) and headless DNS, an initContainer derives this-server-name from the ordinal, and the peer URLs are DNS names (never pod IPs, so the config hash never loops).

Startup preconditions live in a kea-init initContainer (a committed, shellcheck-clean internal/kea/scripts/init.sh embedded via go:embed and parameterised by env vars — no shell is interpolated in Go). It hardens the shared run dir to 0750, substitutes this-server-name from the pod ordinal, stages both configs into the shared emptyDir, and bounded-waits for the HA peer DNS to resolve (kea-dhcp4 -t, failing loud after the cap so the kubelet restarts it). The main kea-dhcp4 and kea-ctrl-agent containers then exec kea directly with no wrapper shell.

Anycast routing caveat (deployment follow-up)

The DHCP Service is a LoadBalancer intended to receive a PureLB anycast IP; routers relay unicast to it. Under hot-standby the standby does not answer while the primary is up, so the anycast VIP should be pinned to the active peer. Pin it operationally (PureLB local traffic policy / active-peer endpoint selection) before production cutover. See follow-ups.

REST API (KeaAPI)

A separate binary (cmd/api) spawned by the KeaAPI CRD. It is a Terraform- friendly CRUD facade over the KeaSubnet / KeaClientClass CRs — an alternative to argocd-managed CRs. Contract mirrors encapi:

  • PUT/GET/DELETE /api/v1/subnets/{name} and .../clientclasses/{name}, plus list endpoints. Stable client-supplied IDs (the CR name, from the URL path).
  • PUT is an idempotent upsert returning the canonical object (200); DELETE returns 204; GET on a missing resource returns 404 (drives provider drift handling). Error bodies are {"error": "..."}.
  • Auth: bearer token (constant-time compare, fail-closed) from KEA_API_TOKEN, sourced from a k8s Secret the operator generates if absent (or pre-seeds).

The Terraform provider is a separate queued task; the API is designed so wrapping it is trivial (full-object PUT, canonical GET, 404 semantics).

Build & release

  • make testgo test -race over ./api/... ./internal/....
  • make generate — regenerates deepcopy, CRDs, RBAC, and the install.yaml bundle (all committed).
  • make patch|minor|major — tags vX.Y.Z and pushes; the tag triggers the .woodpecker/docker.yaml image builds.

Images: kea-operator and kea-api are distroless Go binaries. The Kea workload image (Dockerfile.kea) is built from AlmaLinux (reachable via the artifactapi dockerhub remote) + EPEL kea packages.

Follow-ups (not in this repo yet)

  • argocd-apps: a kea-operator-ci ServiceAccount for the woodpecker CI steps, and the operator deployment manifests.
  • artifactapi: the Kea image build pulls kea RPMs from EPEL; if the CI build network cannot reach EPEL, add an artifactapi rpm remote (or vendor kea via rpmbuilder) and point Dockerfile.kea at it.
  • terraform-provider-kea: wrap the KeaAPI REST contract.
  • Vault: issue ephemeral API bearer tokens instead of a static k8s Secret.
  • Anycast: pin the anycast VIP to the active HA peer (PureLB tuning).
S
Description
Kubernetes operator for managing Kea DHCP clusters, subnets, and PXE client classes
Readme 200 KiB
Languages
Go 97%
Makefile 1.6%
Shell 1.4%