Deterministic LoadBalancer IP Assignment for the Kube API Server
Technology Preview — Day 1 only
Deterministic LoadBalancer IP assignment is a Technology Preview feature. It requires the management cluster feature gate described in Prerequisites and is not supported in production environments.
This documentation covers Day 1 only: selecting the LoadBalancer class and the annotations that request a specific address when the HostedCluster is created. The fields are immutable afterwards. Changing the kube-apiserver LoadBalancer IP on a running cluster (Day 2) is not implemented in OCP 5.1. See Day 2: Planned behaviour (not implemented) before you plan any IP change.
Overview
By default, when the kube-apiserver is published with the LoadBalancer strategy, HyperShift
lets the load balancer controller on the management cluster choose the address. That address
can be hard to predict before the cluster exists, which makes firewall rules, DNS records, and
security allowlists difficult to prepare.
This feature adds two provider-agnostic fields to the APIServer entry in
HostedCluster.spec.services:
| Field | Purpose |
|---|---|
loadBalancerClass |
Selects which load balancer controller provisions the kube-apiserver Service. Maps to Service.spec.loadBalancerClass. |
serviceAnnotations |
Opaque annotation passthrough to the kube-apiserver Service. The annotations request the address and configure provider-specific behaviour. |
HyperShift passes the values to the kube-apiserver Service and the load balancer controller assigns the requested address. HyperShift does not interpret provider-specific annotation values.
What Day 1 provides
| Capability | Status |
|---|---|
Set loadBalancerClass for the kube-apiserver Service at creation |
Implemented (Technology Preview) |
Pass serviceAnnotations to the kube-apiserver Service at creation |
Implemented (Technology Preview) |
| Apply the address annotation to the primary kube-apiserver Service (public or private) | Implemented (Technology Preview) |
| Azure-specific kube-apiserver LoadBalancer Service on port 7443 | Implemented (Technology Preview) |
Report CPO support through the DeterministicLBIPSupported condition |
Implemented (Technology Preview) |
What Day 1 does not provide
- No runtime IP changes. You cannot change
loadBalancerClassorserviceAnnotationsafter the HostedCluster is created. Day 2 is a separate, unimplemented scope. - No passthrough for other services. The API accepts the fields on any service entry, but
the Control Plane Operator only applies them to the
APIServerservice. Values set onOAuthServer,Konnectivity, orIgnitionare ignored. - No typed per-provider API. Annotation keys and values are not semantically validated beyond the allowlist and size limits described in Validation rules.
- No automated DNS management. On self-managed platforms without ExternalDNS, you must update DNS records yourself.
- No hostname indirection. Day 1 controls IP addresses only.
Day 1 compared with Day 2
| Behaviour | Day 1 (OCP 5.1) | Day 2 (planned, not implemented) |
|---|---|---|
| Set the address at cluster creation | Supported | Supported |
| Change the address on a running cluster | Not supported; the fields are immutable | Planned, without a worker rollout |
serviceAnnotations mutability |
Immutable | Planned to be mutable |
| Public bare-metal kubelet path | Through node-local HAProxy when KubeletHAProxyRouting is enabled; both gates ship in TechPreviewNoUpgrade, but the address is static |
Through HAProxy with runtime refresh from /kas-ip |
| NodePool version requirement | None | Every NodePool must support Day 2 |
How it works
- You create the HostedCluster with
loadBalancerClassandserviceAnnotationson theAPIServerservice entry. Both fields require theDeterministicLBIPmanagement cluster feature gate. - The HyperShift Operator mirrors the HostedCluster spec to the HostedControlPlane.
- The Control Plane Operator reconciles the kube-apiserver Service:
- It sets
Service.spec.loadBalancerClasswhen the field is present. - It writes each
serviceAnnotationsentry as a per-key annotation. - The load balancer controller that watches the selected class processes the annotations and assigns the requested address.
- The existing
Service.Status.LoadBalancer.Ingressflow propagates the address to the kubeconfig and to the data plane configuration.
graph LR
HC[HostedCluster<br/>spec.services APIServer] --> HCP[HostedControlPlane]
HCP --> CPO[Control Plane Operator]
CPO --> SVC[kube-apiserver Service<br/>class + annotations]
SVC --> LBC[Load balancer controller]
LBC --> IP[Requested IP assigned]
IP --> STATUS[Service status / kubeconfig]
Prerequisites
-
A management cluster where you can enable the
DeterministicLBIPfeature gate. EnablingTechPreviewNoUpgradeat install time also enables this gate:hypershift install --tech-preview-no-upgradeThe gate controls availability of the API fields, the HyperShift Operator, and the Control Plane Operator. Without the gate, the fields are not part of the HostedCluster CRD schema: values are dropped by the API server and have no effect. See Feature Gates for the difference between management cluster and hosted cluster feature gates.
-
A load balancer controller on the management cluster that can provision the addresses and annotations you plan to use. For example, MetalLB with an
IPAddressPool, the AWS load balancer controller with Elastic IPs, or Cilium LB IPAM with a pool. - The addresses, pools, and any provider resources (Elastic IP allocations, floating network IDs, and so on) planned and available before cluster creation.
- DNS and firewall rules prepared for the requested address. HyperShift does not create DNS records for this feature on self-managed platforms.
- The kube-apiserver published with the
LoadBalancerstrategy. TheloadBalancerblock is rejected at admission with other strategy types. - A HostedCluster release whose Control Plane Operator supports the feature. The
loadBalancerClassis applied only when the kube-apiserver Service is created, so the first reconciliation must use a supported Control Plane Operator. See Upgrade, downgrade, and version skew.
Configure the HostedCluster
The hypershift CLI does not expose flags for these fields. Render the HostedCluster manifest,
add the fields under spec.services, and apply the manifest.
Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
loadBalancerClass |
string | Optional | Kubernetes qualified name. Maximum 253 characters, name segment maximum 63 characters. Omit it when the management cluster has a single load balancer controller. |
serviceAnnotations |
map[string]string | Optional | Minimum 1 and maximum 10 entries. Keys must match the allowlist and be valid qualified names of at most 317 characters. Values are limited to 253 characters. |
Both fields are under:
spec:
services:
- service: APIServer
servicePublishingStrategy:
type: LoadBalancer
loadBalancer:
loadBalancerClass: <class>
serviceAnnotations:
<key>: <value>
Example: MetalLB with a fixed IP
spec:
services:
- service: APIServer
servicePublishingStrategy:
type: LoadBalancer
loadBalancer:
loadBalancerClass: metallb.io/metallb
serviceAnnotations:
metallb.io/loadBalancerIPs: "10.1.1.100"
- service: OAuthServer
servicePublishingStrategy:
type: Route
- service: Konnectivity
servicePublishingStrategy:
type: Route
- service: Ignition
servicePublishingStrategy:
type: Route
Important
metallb.io/metallb is only a valid class if MetalLB is started with a matching
--lb-class=metallb.io/metallb (or the equivalent Helm value). MetalLB versions that are
not configured with a class only process Services without a class. If you use the
default MetalLB configuration, omit loadBalancerClass and keep serviceAnnotations.
Render the HostedCluster manifest, edit it to add the loadBalancer fields, and apply it to
create the cluster:
hypershift create cluster <platform> \
... \
--render --render-into hostedcluster.yaml
# Edit hostedcluster.yaml and add the loadBalancer fields, then:
oc apply -f hostedcluster.yaml
Secrets are not rendered by default
--render omits Secret objects, including the pull secret and the default encryption key
referenced by the HostedCluster. Before applying the manifest, make sure the referenced
pull secret and any other required Secrets already exist in the target namespace, or add
--render-sensitive to include them. A file rendered with --render-sensitive contains
credentials: store it securely and delete it after use.
Provider examples
The annotation allowlist accepts the following prefixes. The examples are illustrative: your controller must be configured to watch the selected class, and annotation keys and values must match what that controller expects.
| Provider | loadBalancerClass |
Example serviceAnnotations |
|---|---|---|
| MetalLB | Optional; metallb.io/metallb only with a matching --lb-class |
metallb.io/loadBalancerIPs: "10.1.1.100" or metallb.io/address-pool: "production" |
| MetalLB (legacy prefix) | Same as above | metallb.universe.tf/loadBalancerIPs: "10.1.1.100" |
| AWS NLB | Omit (CCM) or set the class your controller watches | service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-abc123" |
| Azure | Omit | service.beta.kubernetes.io/azure-load-balancer-ipv4: "20.0.0.100" |
| GCP | Omit, or networking.gke.io/l4-regional-external when the backend-service-based controller is required |
networking.gke.io/load-balancer-ip-addresses: "my-ip" |
| F5 BIG-IP CIS | f5.com/f5-bigip |
cis.f5.com/ip: "10.2.2.50" or cis.f5.com/ipamLabel: "production" |
| Cilium | io.cilium/bgp-control-plane or io.cilium/l2-announcer |
lbipam.cilium.io/ips: "10.3.3.50" |
| OpenStack | Omit | loadbalancer.openstack.org/floating-network-id: "<uuid>" selects the floating IP network, not a specific address |
AWS Elastic IPs
The EIP example assumes the AWS cloud-controller-manager (CCM) path. If the AWS Load
Balancer Controller mutates Services, its default scheme is internal, and EIP allocations
require an internet-facing load balancer. The
service.beta.kubernetes.io/aws-load-balancer-scheme annotation is not in the allowlist,
so serviceAnnotations cannot select the scheme. Use a management cluster configuration
where the controller creates an internet-facing load balancer, or scope this example to
CCM. The number of EIP allocations must match the number of selected subnets.
GCP load balancer controller
On GKE, networking.gke.io/load-balancer-ip-addresses is honored by the
backend-service-based L4 controller. If the management cluster still uses the legacy
external load balancer path, set
loadBalancerClass: networking.gke.io/l4-regional-external. Otherwise the reserved
address may not be honored. Check your GKE version and controller configuration.
OpenStack does not pin a specific address
loadbalancer.openstack.org/floating-network-id selects the external network used to
allocate a floating IP; it does not pin a specific address. The OpenStack cloud provider
takes a specific floating IP from Service.spec.loadBalancerIP, which this feature does
not expose in Day 1. OpenStack appears here because its annotation prefix is in the
allowlist, not because it provides deterministic address assignment.
Multiple load balancer controllers
Setting loadBalancerClass is only useful when the management cluster runs more than one
load balancer controller. The Service stays Pending until a controller that watches that
exact class claims it. Otherwise omit loadBalancerClass and let the default controller
provision the Service, unless your provider requires a class (for example GKE's
backend-service-based controller).
Which Service receives the values
The kube-apiserver can be exposed through a public Service, a private Service, or both, depending on the endpoint access configuration:
| Endpoint access | loadBalancerClass |
serviceAnnotations |
|---|---|---|
Public |
Public kube-apiserver Service | Public kube-apiserver Service |
Private |
Private kube-apiserver Service | Private kube-apiserver Service |
PublicAndPrivate |
Both kube-apiserver LoadBalancer Services | Public kube-apiserver Service only |
The address annotation is applied to the Service that exposes the primary endpoint only.
Requesting the same address on both Services can leave one of them Pending. The class is
applied to every kube-apiserver Service that is of type LoadBalancer, because it selects the
provisioning controller rather than an address.
For Azure, HyperShift uses a dedicated kube-apiserver LoadBalancer Service on port 7443 instead of 6443. The deterministic fields are applied to that Service.
Validation rules
The API validates the fields at admission time with CEL. Invalid values are rejected when the HostedCluster is created.
serviceAnnotations
- Minimum 1 and maximum 10 annotations per service entry.
-
Keys must start with one of these prefixes:
Prefix Provider metallb.io/MetalLB (current) metallb.universe.tf/MetalLB (legacy) service.beta.kubernetes.io/aws-load-balancer-eipAWS NLB Elastic IP service.beta.kubernetes.io/aws-load-balancer-subnetAWS NLB subnet service.beta.kubernetes.io/aws-load-balancer-target-groupAWS NLB target group service.beta.kubernetes.io/azure-load-balancer-ipvAzure IPv4/IPv6 address service.beta.kubernetes.io/azure-load-balancer-resource-groupAzure resource group networking.gke.io/GCP, except networking.gke.io/load-balancer-typecis.f5.com/F5 BIG-IP CIS lbipam.cilium.io/Cilium LB IPAM loadbalancer.openstack.org/OpenStack -
networking.gke.io/load-balancer-typeis explicitly rejected because it controls whether the load balancer is internal or external. - Keys must be valid qualified names: an optional DNS subdomain prefix,
/, and a name segment that starts and ends with an alphanumeric character. Keys are limited to 317 characters and the name segment to 63 characters. - Values are limited to 253 characters.
- The allowlist validates the provider prefix only. It does not validate that the annotation key or value is correct for the provider.
loadBalancerClass
- Optional. When set, it must be a valid Kubernetes qualified name, at most 253 characters, with a name segment of at most 63 characters.
- Kubernetes makes
Service.spec.loadBalancerClassimmutable once set. The HostedCluster fields follow the same lifetime.
Verify the configuration
After the cluster is created, check the HostedControlPlane namespace. The namespace is usually
<hostedcluster-namespace>-<hostedcluster-name>.
# Check the class on the kube-apiserver Service.
oc get svc kube-apiserver -n <hcp-namespace> \
-o jsonpath='{.spec.loadBalancerClass}{"\n"}'
# Check the annotations.
oc get svc kube-apiserver -n <hcp-namespace> \
-o jsonpath='{.metadata.annotations}{"\n"}'
# Check the assigned address.
oc get svc kube-apiserver -n <hcp-namespace> \
-o jsonpath='{.status.loadBalancer.ingress[0]}{"\n"}'
For clusters with PublicAndPrivate endpoint access, the address annotation is expected on the
public Service, not on kube-apiserver-private. For Private endpoint access, check
kube-apiserver-private.
For public Azure clusters (including KubeVirt on Azure), the LoadBalancer Service is named
kube-apiserverlb; use that name in the commands above. The kube-apiserver Service still
exists as a ClusterIP Service on those clusters and does not carry the LoadBalancer fields.
Confirm that the Control Plane Operator supports the fields:
oc get hostedcluster <name> -n <namespace> \
-o jsonpath='{.status.conditions[?(@.type=="DeterministicLBIPSupported")]}{"\n"}'
True means the Control Plane Operator honors the fields, or that the HostedCluster does not
use them.
Operate the annotations
- HyperShift writes
serviceAnnotationsentries individually. It does not replace the full annotation map. - Annotations set by other controllers, such as the MetalLB pool status, are preserved across reconciliation.
- For keys that pass the allowlist and are also written by HyperShift, HyperShift's value
wins. On AWS,
service.beta.kubernetes.io/aws-load-balancer-typeis managed by HyperShift but is not in the allowlist, so setting it inserviceAnnotationsis rejected at admission rather than silently skipped. - Removing an annotation from the HostedCluster is not possible in Day 1 because the fields are immutable.
Upgrade, downgrade, and version skew
- Existing clusters without the fields behave exactly as before. The fields are optional and have no defaults.
- New HyperShift Operator with an older Control Plane Operator can happen because the
Control Plane Operator version follows the HostedCluster release image. The Control Plane
Operator then ignores the fields. HyperShift reports this through the
DeterministicLBIPSupportedcondition with statusFalseand reasonUnsupportedControlPlaneOperator. Upgrading the HostedCluster control plane release image makes the Control Plane Operator applyserviceAnnotationson the next reconciliation, but it does not retrofitloadBalancerClassonto a Service that was already created without it. The class is set only when the Service is created, or when an existing Service transitions to typeLoadBalancer. If the older Control Plane Operator already reconciled the kube-apiserver Service, create the HostedCluster again with the fields so the class is set at creation. - Downgrading the HyperShift Operator is not supported for clusters using this feature. The downgraded CRD schema does not contain the fields. Kubernetes can prune them when it decodes or writes the stored HostedCluster, so the configuration may be lost, and re-upgrading does not guarantee that it is restored. Depending on when pruning happens, updates can be rejected or the object can be persisted without the fields, which can leave the HostedCluster blocked until the operator is upgraded again. Annotations already written to the kube-apiserver Service persist, and the load balancer continues to serve the assigned address.
- Adding the fields to an existing cluster is not supported in Day 1 (Technology Preview).
In Day 1,
spec.servicesis immutable for all platforms except IBM Cloud, so create a new HostedCluster with the fields instead.loadBalancerClassremains immutable after creation in all phases because Kubernetes makesService.spec.loadBalancerClassimmutable. ChangingserviceAnnotationson an existing cluster is planned for Day 2.
Limitations
- Day 1 only. No runtime address changes.
- Only the
APIServerservice is processed. Other services accept the fields but ignore them. loadBalancerconfiguration is only valid with theLoadBalancerpublishing strategy.Route,NodePort,None, andS3reject the block at admission.- HyperShift reads only the first entry of
Service.Status.LoadBalancer.Ingressto determine the control plane endpoint. In dual-stack environments, use an address from the Service's primary IP family, which is the family of that first entry. A secondary-family address is not consumed by the control plane endpoint. - No semantic validation of provider annotation keys or values. A misspelled key inside an
allowed prefix passes admission; the controller ignores it, and the Service may stay
Pendingor fall back to a dynamically assigned address. - A
loadBalancerClasswith no matching controller leaves the ServicePendingindefinitely. - No automatic DNS management on self-managed platforms.
- No support for PowerVS or IBM Cloud. The feature targets the load balancer providers in the allowlist.
Troubleshooting
Admission rejects serviceAnnotations
Look for messages such as:
serviceAnnotations keys must start with a known load balancer provider prefixannotation keys must be valid qualified namesannotation keys must be at most 317 charactersannotation values must be at most 253 charactersserviceAnnotations in body should have at least 1 itemsorserviceAnnotations in body should have at most 10 items
Fix the keys or values in the HostedCluster manifest and create the cluster again.
Admission rejects an update
If you try to change or add the fields after creation, the API returns an error similar to:
Services is immutable. Changes might result in unpredictable and disruptive behavior.
This is expected in Day 1. Create a new HostedCluster with the desired values. IBM Cloud is
exempt from the spec.services immutability rule.
The kube-apiserver Service stays Pending
Check the following:
-
The Service has the expected class. On public Azure clusters the name is
kube-apiserverlb:oc get svc kube-apiserver -n <hcp-namespace> -o yaml # Public Azure only: oc get svc kube-apiserverlb -n <hcp-namespace> -o yaml -
A load balancer controller on the management cluster watches that class. If you set a class that no controller handles, the Service never gets an address. Omit the class if you are unsure.
- The requested address or pool exists and has free addresses.
- The annotation key is spelled exactly as the provider expects. The allowlist checks only the prefix.
- For private clusters, inspect
kube-apiserver-private. ForPublicAndPrivateclusters, inspect the public Service; the address annotation should not be on the private Service.
DeterministicLBIPSupported is False
The Control Plane Operator image in use predates the feature and ignores the fields. Upgrade
the HostedCluster control plane release image so that the Control Plane Operator knows about
the fields. Upgrading applies serviceAnnotations on the next reconciliation, but it does not
add loadBalancerClass to a Service that was created without it. If the kube-apiserver Service
already exists, create the HostedCluster again with the fields so the class is set at creation.
The API server is not reachable at the requested address
Verify that the load balancer actually assigned the address, then check the network path:
- Security groups, firewall rules, and routing allow traffic to the address.
- DNS records point to the address, if you use DNS.
- The load balancer controller reports a healthy backend for the kube-apiserver Service.
Support procedures
- Detect: inspect
Service.Status.LoadBalancer.Ingressand the HostedCluster conditions. An empty ingress after the expected provisioning time indicates a load balancer problem. - Diagnose: compare the annotations on the kube-apiserver Service with the provider's documentation. Check the load balancer controller logs on the management cluster.
- Disable: because the fields are immutable in Day 1, you cannot remove them from an existing HostedCluster. To return to dynamic address assignment, create a new HostedCluster without the fields.
Day 2: Planned behaviour (not implemented)
Not implemented — do not rely on this section
Everything in this section describes the intended design for Day 2. It is not implemented in OCP 5.1 and the details are subject to change. Day 2 is tracked separately in CNTRLPLANE-4796 and OCPSTRAT-3926. Do not attempt to change the kube-apiserver LoadBalancer IP on a running cluster today.
Day 2 targets changing the kube-apiserver LoadBalancer address on a running HostedCluster without rolling the data plane nodes. The intended design is:
serviceAnnotationsis planned to become mutable after creation. You change the address annotation on the HostedCluster.- The HyperShift Operator mirrors the change to the HostedControlPlane, and the Control Plane Operator applies the new annotations to the kube-apiserver Service.
- The load balancer controller assigns the new address and updates the Service status.
- A sidecar in the HAProxy static pod polls a new
/kas-ipendpoint on the ignition server. When the address changes, the sidecar rewrites the HAProxy configuration and sendsSIGUSR2to reload it. - Kubelet reaches the API server through the node-local HAProxy instead of the LoadBalancer address. This prevents kubelet from being stranded with an old address.
Day 2 also introduces version-skew protection:
- A HostedCluster condition,
ValidLoadBalancerIPConfiguration, reports whether every NodePool supports the runtime address change. - A NodePool condition,
ValidLoadBalancerIPSupport, reports whether that NodePool supports it. - NodePools from releases without the sidecar and the
/kas-ipendpoint are reported as unsupported. Changing the address while any NodePool is unsupported is expected to be blocked or flagged by the condition.
Prerequisites and implications for Day 2:
- All NodePools must run a release that includes the sidecar and the
/kas-ipendpoint. - On public bare-metal clusters, kubelet must route through HAProxy. That routing is
implemented today behind the
KubeletHAProxyRoutingmanagement cluster feature gate (TechPreviewNoUpgrade), but the Day 2 address-change mechanism itself is not. - A brief connectivity window while the new address is propagated is expected.
Until Day 2 is implemented, treat the LoadBalancer address as immutable and plan cluster recreation if the address must change.