LoadBalancer
A LoadBalancerClass configures how Lace assigns and advertises external VIPs for LoadBalancer Services. A Service opts in by setting spec.loadBalancerClass to lace-cni.io/<metadata.name>; the class then decides which address pool the VIP comes from and how it is advertised to the outside network.
Only class names carrying the lace-cni.io/ prefix are handled by Lace. A Service whose loadBalancerClass has the prefix but matches no class is reported as an error rather than silently left to another implementation.
A Service that sets no loadBalancerClass at all is adopted by the default class, if there is one.
A Service carries the standard service.kubernetes.io/load-balancer-cleanup finalizer for as long as Lace advertises a VIP for it, so deleting it completes only once the address is released. A Service Lace advertises nothing for never receives one.
apiVersion: lace-cni.io/v1alpha1kind: LoadBalancerClassmetadata: name: publicspec: mode: l2 ipv4Pools: - cidr: 198.51.100.0/24 - start: 203.0.113.10 end: 203.0.113.20 ipv6Pools: - cidr: 2001:db8:f00d::/64A Service selects the class above with loadBalancerClass: lace-cni.io/public.
The default class
Section titled “The default class”Where Lace is the LoadBalancer implementation of a cluster, one class can be marked as the cluster default, so a type: LoadBalancer Service that names no class at all just works.
apiVersion: lace-cni.io/v1alpha1kind: LoadBalancerClassmetadata: name: public annotations: loadbalancer.lace-cni.io/default-class: "true"- Exactly one class may carry the marker. With more than one there is no default and nothing is adopted; with none, a Service that names no class is left to another implementation.
spec.loadBalancerClasswins wherever it is set: the default adopts only Services that name no class, never another implementation’s.- Adoption is sticky. An adopted Service keeps its class and its VIP when the default later moves to another class or is dropped.
- Kubernetes rejects filling in
spec.loadBalancerClassafter the fact, so the resolved class is instead recorded on the Service as theloadbalancer.lace-cni.io/class-nameannotation. It is informational only — editing it rebinds nothing.
Each class carries an ipv4Pools and/or ipv6Pools list; at least one must be non-empty. Each entry is either a cidr block or an inclusive start–end range. Entries are tried in order, falling through to the next when one is exhausted.
A Service gets one VIP per address family it is configured for, each allocated from the matching family’s pools. The families a Service can have are its own IP-family configuration, further constrained to the families the class actually provides a pool for.
Advertisements
Section titled “Advertisements”spec.mode selects how an allocated VIP is advertised to the surrounding network. Only l2 is implemented; l3 is reserved for a future BGP-based advertisement.
In L2 mode each allocated VIP is tracked by an L2Advertisement — one per VIP — which records the VIP, the owning Service, and the elected node. The VIP is attracted to a single node that answers for it on the local segment:
- Exactly one node is elected to advertise each VIP. The L2Advertisement doubles as the election lock: the winning node claims it and heartbeats its liveness, and a stale claim is taken over by another reachable node.
- The elected node answers ARP (IPv4) and NDP (IPv6) for the VIP, and on acquisition sends a gratuitous ARP / unsolicited Neighbor Advertisement so switches and peers re-point the VIP to it immediately.
- Only a node that can actually reach the VIP’s subnet is a candidate; a node with no route to the VIP never claims it.
The election only decides where external traffic enters the cluster. Routing the VIP onward to the Service’s endpoints is not tied to the elected node: every node programs the VIP into its data plane and load-balances it across the Service’s endpoints wherever they run. So the entry node forwards to a backing pod on any node, and a takeover changes only the ingress point, not reachability.
LoadBalancerClass fields
Section titled “LoadBalancerClass fields”spec.ipv4Pools,spec.ipv6Pools— the address pools VIPs are allocated from; at least one must be non-empty. Entries are tried in order, falling through to the next when one is exhausted. Each entry is either:cidr— a CIDR block, orstartandend— an inclusive address range.
spec.mode— the advertisement mechanism. Onlyl2is implemented (the default);l3is reserved for a future BGP-based advertisement.
The L2Advertisement resource
Section titled “The L2Advertisement resource”- Lifecycle — created and owned by the controller: one per LoadBalancer Service per required IP family, under a deterministic name. The controller allocates its VIP from the class pool and removes it when the family is no longer needed or the Service is deleted.
- Contents — the allocated VIP, the owning Service, the class it was drawn from, and its IP family; plus the elected node and a liveness timestamp it heartbeats.
- Use — nodes elect a single owner per advertisement through its status and answer for the VIP on the segment, as described under L2.
Requesting a specific VIP
Section titled “Requesting a specific VIP”A Service can ask for specific addresses with the loadbalancer.lace-cni.io/loadBalancerIPs annotation: a comma-separated list of at most one IPv4 and one IPv6 address. Each must fall within the class’s pools, and is claimed exactly if free.
apiVersion: v1kind: Servicemetadata: name: web annotations: loadbalancer.lace-cni.io/loadBalancerIPs: "198.51.100.7,2001:db8:f00d::7"spec: type: LoadBalancer loadBalancerClass: lace-cni.io/public # ...A requested address that is already taken or outside every pool is reported as an error on the Service instead of being silently replaced.