Bare-metal Kubernetes controller that assigns LoadBalancer IPs from IPv4/IPv6 address pools, with namespace selectors, per-pool quotas and Prometheus metrics.
Helios Load Balancer is a Kubernetes controller for bare metal clusters that gives LoadBalancer services an external IP. It allocates addresses from the pools you define and writes them to the Service status.
- Automatic IP address allocation for LoadBalancer services
- Support for IP ranges in CIDR, range, or single IP format
- IPv4 and IPv6 dual-stack support
- Multiple HeliosConfig resources per cluster with independent IP ranges
- Namespace isolation via
namespaceSelector - Per-config IP allocation quota via
maxAllocations - CRD schema validation (CEL) for port/weight uniqueness, method-weight consistency, and health check config
- Optional validating webhook for IP range format and cross-config overlap validation
- Prometheus metrics support
- Status monitoring and reporting
The method, weights and healthCheck fields are accepted and validated, but the controller does not act on them yet. helios-lb also does not announce addresses on the network (no ARP or BGP). Traffic to a Service is distributed by kube-proxy as usual, and the address pool has to be reachable through your own network, for example with routes to your nodes.
The controller:
- Watches for services of type LoadBalancer
- Allocates IP addresses from configured IP ranges
- Updates service status with allocated external IPs
Helios Load Balancer can coexist with MetalLB in the same cluster if properly configured:
-
Configure different IP ranges for each load balancer:
# MetalLB IPAddressPool apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: first-pool namespace: metallb-system spec: addresses: - 192.168.1.100-192.168.1.200 # MetalLB range
# Helios-LB HeliosConfig apiVersion: balancer.helios.dev/v1 kind: HeliosConfig metadata: name: heliosconfig-sample spec: ipRange: "192.0.2.65" # Helios-LB range
-
When creating a LoadBalancer service, specify which load balancer should handle it by using the appropriate IP range:
# Service using MetalLB apiVersion: v1 kind: Service metadata: name: metallb-service spec: type: LoadBalancer loadBalancerIP: "192.168.1.100" # MetalLB range
# Service using Helios-LB apiVersion: v1 kind: Service metadata: name: helios-service spec: type: LoadBalancer loadBalancerIP: "192.0.2.65" # Helios-LB range
This way, you can leverage both load balancers in your cluster, each managing its own IP range.
spec.loadBalancerIP does double duty here. It selects which load balancer picks
up the Service, and Helios-LB then assigns exactly that address — the request is
honored, never silently swapped for another one. If the address is already taken
by another Service, allocation fails and the reason is recorded on the
HeliosConfig status rather than a different IP being handed out. Omit the field
to take the next free address from the pool.
The address must be one the pool can actually hand out. For a CIDR range that
excludes the network and broadcast addresses, so 192.0.2.0 and 192.0.2.255
are not valid requests against 192.0.2.0/24.
- Kubernetes v1.25+ (the CRD ships CEL validation rules, which require
x-kubernetes-validations) - kubectl v1.11.3+
Recommended: OCI registry (Helm 3.8+)
# Single-command install — no helm repo add needed
helm install helios-lb oci://ghcr.io/somaz94/charts/helios-lb \
--version 0.8.1The chart creates the helios-lb-system namespace itself (namespace in its values) and deploys the controller there, so install it without --namespace and --create-namespace: with them, the install fails with namespaces "helios-lb-system" already exists.
Alternative: classic Helm repo
# Add the Helm repository
helm repo add helios-lb https://somaz94.github.io/helios-lb/helm-repo
helm repo update
# Install with default values
helm install helios-lb helios-lb/helios-lb
# Or install with custom values
helm install helios-lb helios-lb/helios-lb \
--set image.tag=v0.8.1For full Helm chart options, see Helm README.
kubectl apply -f https://raw.githubusercontent.com/somaz94/helios-lb/main/dist/install.yaml# Clone the repository
git clone https://github.com/somaz94/helios-lb.git
cd helios-lb
# Install CRDs
make install
# Deploy the controller
make deploy IMG=somaz940/helios-lb:v0.8.1apiVersion: balancer.helios.dev/v1
kind: HeliosConfig
metadata:
name: heliosconfig-sample
spec:
# IP range for virtual IP allocation (CIDR or range format) default port: 80 & default protocol: tcp
# Supports: single IP, range (192.168.1.100-200), CIDR (192.168.1.0/24), IPv6 (fd00::1, fd00::/120)
ipRange: "192.0.2.65"
ipv6Range: "fd00::1" # Optional: enables dual-stack (IPv4 + IPv6)
method: RoundRobin # RoundRobin, LeastConnection, WeightedRoundRobin, IPHash, Random
ports: # Optional: default is 80
- port: 80
- port: 443
protocol: TCP # Optional: default is TCP
namespaceSelector: # Optional: restrict to specific namespaces
- default
- production
maxAllocations: 10 # Optional: limit IP allocations (0 = unlimited)
# Download the sample yaml file
# default port: 80 & default protocol: tcp
curl -o heliosconfig.yaml https://raw.githubusercontent.com/somaz94/helios-lb/main/release/examples/balancer_v1_heliosconfig.yaml
# port: 80,443 & protocol: tcp
curl -o heliosconfig-port.yaml https://raw.githubusercontent.com/somaz94/helios-lb/main/release/examples/balancer_v1_heliosconfig_port.yaml
# dual-stack (IPv4 + IPv6)
curl -o heliosconfig-dualstack.yaml https://raw.githubusercontent.com/somaz94/helios-lb/main/release/examples/balancer_v1_heliosconfig_dualstack.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
labels:
app: nginx-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test
spec:
type: LoadBalancer
loadBalancerClass: helios-lb
loadBalancerIP: "192.0.2.65"
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: nginx-test
# Download the sample yaml file
curl -o nginx-test.yaml https://raw.githubusercontent.com/somaz94/helios-lb/main/release/examples/nginx-test.yamlCheck the service status:
kubectl get svc nginx-test
...
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-test LoadBalancer 10.96.0.189 192.0.2.65 80:30396/TCP 2s
k get heliosconfigs
NAME PHASE MESSAGE AGE
heliosconfig-sample-port Active IP allocated successfully 26sYou should see an external IP assigned from your configured IP range.
ipRange: IP range for allocation (required) - supports single IP (192.168.1.100), range format (192.168.1.100-192.168.1.200), CIDR format (192.168.1.0/24), and IPv6 (fd00::1,fd00::1-fd00::ff,fd00::/120)ipv6Range: IPv6 address range for dual-stack allocation (optional) - when set alongsideipRange, enables dual-stack mode where both IPv4 and IPv6 addresses are allocated to each servicemethod: Load balancing method (RoundRobin,LeastConnection,WeightedRoundRobin,IPHash,Random)ports: Port configuration for the service (default: 80)protocol: Protocol type (default: TCP)weights: Per-service backend weights for WeightedRoundRobin (optional)serviceName: Name of the Kubernetes serviceweight: Relative weight (1-100, default: 1)
namespaceSelector: List of namespaces this config manages (optional, empty = all namespaces)maxAllocations: Maximum number of IP allocations for this config (optional, 0 = unlimited)healthCheck: Health check configuration (optional)enabled: Enable/disable health checking (default: true)intervalSeconds: Interval between health checks in seconds (default: 5, range: 1-300)timeoutMs: Health check timeout in milliseconds (default: 1000, range: 1-30000)protocol: Health check protocol -TCPorHTTP(default: TCP)httpPath: HTTP path for HTTP health checks (e.g.,/healthz)
allocatedIPs: Map of service names to their allocated IPv4 addressesallocatedIPv6s: Map of service names to their allocated IPv6 addresses (dual-stack only)phase: Current phase of the HeliosConfig (Pending,Active,Failed)state: Current state (same as phase, for backward compatibility)message: Human-readable status messageconditions: Standard Kubernetes conditions:Ready: Whether the HeliosConfig is successfully allocating IPsDegraded: Whether there are issues (e.g., IP conflicts)
When multiple HeliosConfig resources exist in a cluster, Helios-LB automatically detects IP range overlaps:
- If an IP already allocated by another HeliosConfig falls within the current config's range, a conflict is reported
- Conflicting configs are marked with
Degraded=Truecondition andIPConflictreason - A Kubernetes warning event
IPConflictis emitted - The controller requeues with a 30-second delay to allow resolution
The controller emits the following events on HeliosConfig resources:
| Event | Type | Description |
|---|---|---|
IPAllocated |
Normal | IP successfully allocated to a service |
IPConflict |
Warning | IP range overlaps with another HeliosConfig |
QuotaExceeded |
Warning | Max allocations limit reached |
AllocationFailed |
Warning | Failed to allocate IP for a service |
CleanupStarted |
Normal | Releasing allocated IPs during deletion |
CleanupComplete |
Normal | All IPs released and finalizer removed |
These rules are part of the CRD schema (field constraints and CEL x-kubernetes-validations), so they are enforced on every cluster with no extra setup — including when the webhook is disabled:
| Rule | Message |
|---|---|
spec.weights may only be set when spec.method is WeightedRoundRobin |
weights can only be used with the WeightedRoundRobin method |
spec.ports[*].port must be unique |
duplicate port in spec.ports |
spec.weights[*].serviceName must be unique and non-empty |
duplicate serviceName in spec.weights |
spec.healthCheck.httpPath is required when protocol is HTTP |
httpPath is required when the health check protocol is HTTP |
The webhook adds the checks that the CRD schema cannot express — IP range format (single IP / range / CIDR, IPv4 and IPv6) and IP range overlap between different HeliosConfig resources, which requires reading other objects in the cluster.
The webhook is disabled by default and can be enabled in three ways:
-
Without webhook (default): IP range format and cross-config overlap are not checked at admission; a malformed range surfaces as a reconcile error instead. All schema rules in the table above still apply.
-
With webhook + cert-manager: Recommended for production. cert-manager automatically provisions and rotates TLS certificates.
# Helm helm install helios-lb helios-lb/helios-lb \ --set webhook.enabled=true \ --set webhook.certManager.enabled=true # Kustomize: uncomment [WEBHOOK] and [CERTMANAGER] sections in config/default/kustomization.yaml
-
With webhook, without cert-manager: You must manually create a TLS Secret named
webhook-server-certin the controller namespace containingtls.crtandtls.key.
The controller uses the following system ports:
- Health probe endpoint:
:9082 - Metrics endpoint (if enabled):
:8443
These ports should be available when running the controller with hostNetwork enabled.
To ensure proper handling of services between MetalLB and Helios-LB, use the loadBalancerClass field:
apiVersion: v1
kind: Service
metadata:
name: nginx-test
spec:
type: LoadBalancer
loadBalancerClass: helios-lb # Specifies that Helios-LB should handle this service
loadBalancerIP: "192.0.2.65"
ports:
- port: 80
targetPort: 80
selector:
app: nginx-testHelios-LB serves Services whose spec.loadBalancerClass is helios-lb or unset, and adds the annotation balancer.helios.dev/load-balancer-class: helios-lb to the Services it serves. Kubernetes only accepts loadBalancerClass when a Service is created, so the controller cannot set it afterwards; set it yourself, as above. MetalLB then skips the Service, and Helios-LB skips every Service with another class. A Service with no class is picked up by both controllers, so leave none unset while both run.
The method field accepts the following values. The CRD validates them, but the controller does not apply them yet (see Features).
-
Round Robin (Default)
spec: method: RoundRobin
- Distributes requests sequentially across all backends
- Ensures even distribution of traffic
-
Least Connection
spec: method: LeastConnection
- Directs traffic to backend with fewest active connections
- Ideal for long-lived connections
-
Weighted Round Robin
spec: method: WeightedRoundRobin
- Distributes traffic based on backend weights
- Higher-weight backends receive more traffic
-
IP Hash
spec: method: IPHash
- Routes traffic based on client IP hash
- Provides session persistence (same client → same backend)
-
Random
spec: method: Random
- Randomly selects a healthy backend
- Simple and effective for homogeneous backends
Common issues and solutions:
-
IP allocation fails:
- Verify IP range configuration
- Check network interface exists
- Review controller logs
-
Service external IP not assigned:
- Verify HeliosConfig exists and is valid
- Check controller logs for errors
-
Network connectivity issues:
- helios-lb does not announce addresses, so make sure the pool is routed to your nodes
- Ensure IP range is valid for your network
- Delete the LoadBalancer service:
kubectl delete -f https://raw.githubusercontent.com/somaz94/helios-lb/main/release/examples/nginx-test.yaml- Delete the HeliosConfig:
k get heliosconfig
kubectl delete heliosconfig <heliosconfig-name>- Remove the controller:
kubectl delete -f https://raw.githubusercontent.com/somaz94/helios-lb/main/dist/install.yamlAll required tools will be automatically downloaded to ./bin directory when running:
make install-toolsOr you can install individual tools:
# Install controller-gen
make controller-gen # v0.16.4
# Install kustomize
make kustomize # v5.5.0
# Install setup-envtest
make envtest # v0.19.0
# Install golangci-lint
make golangci-lint # v2.1.6Manual installation locations:
- All tools will be installed in
./bindirectory - Specific versions:
- controller-gen v0.16.4
- kustomize v5.5.0
- setup-envtest v0.19.0
- golangci-lint v2.1.6
Note: The binary directory (./bin) is git-ignored and will be created when needed.
| Document | Description |
|---|---|
| Helm Chart | Helm chart installation, configuration, and values reference |
| Troubleshooting | Common issues and solutions |
| Version Bump | Checklist for releasing a new version |
| Contributing | How to contribute to this project |
Issues and pull requests are welcome.
- network-policy-generator: An operator that learns pod traffic and generates NetworkPolicies from it.
- k8s-namespace-sync: An operator that syncs Secrets and ConfigMaps across namespaces.
- kube-drift: An operator that runs the same manifest-vs-cluster comparison on a schedule, inside the cluster.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
