skip to content

How do you configure the cloud load balancer behind a Kubernetes LoadBalancer Service, for example internal-only, and what does spec.loadBalancerClass change?

level: middleimportance: should knowfreq 46%

answer

  1. portable API, thin spec
  2. provider reads annotations
  3. typos silently ignored
  4. class means default skips it
  5. set at creation, immutable

basics

~20 s

The Service spec has only a few portable load-balancer fields, so provider-specific settings such as internal-only, balancer flavour or TLS are set with the provider's annotations on the Service. spec.loadBalancerClass hands the Service to a different load-balancer implementation instead of the default one.

solid answer

~40 s

Kubernetes keeps the Service API portable, so it has only a few load-balancer fields: `type`, `loadBalancerSourceRanges`, `externalTrafficPolicy`, `allocateLoadBalancerNodePorts`, `loadBalancerClass`, and the deprecated `loadBalancerIP`. Everything provider-specific goes in **annotations** that the provider's controller reads: internal-only versus internet-facing, balancer flavour, certificates, timeouts, health checks. The API server does not validate them, so a typo is silently ignored, and some providers only read certain annotations when the balancer is created. `spec.loadBalancerClass` chooses **which implementation** handles the Service. The default cloud implementation skips any Service that sets it, and the field can only be set when you create the Service or switch it to LoadBalancer. After that it cannot be changed.

code

yaml · 8 lines
yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: lb-cap
  namespace: billing-invoices
spec:
  hard:
    services.loadbalancers: "2"

go deeper

for a junior

Remember that balancer settings such as internal-only come from provider annotations on the Service, not from extra spec fields.

for a middle

Explain which fields are portable, why annotations carry the rest, and exactly what loadBalancerClass does: the default implementation skips the Service, and the class is fixed once set.

for a senior

Cover the operational traps: unvalidated typos, annotations applied only at creation that force recreating the Service and changing its address, and tenants flipping a balancer public. Name admission policy and quotas as the controls.

for a principal

Treat one balancer per Service as a cost and exposure policy. Decide which traffic shares an ingress or gateway balancer, which gets a dedicated one, and which implementation each class maps to.

## Why the Service spec is so thin A Kubernetes **Service** of `type: LoadBalancer` has to mean the same thing on every cloud and on bare metal. So the API holds only the fields that make sense everywhere: | Field | What it expresses | Portability | |---|---|---| | `type: LoadBalancer` | "give me an external balancer" | universal | | `loadBalancerSourceRanges` | client CIDRs allowed through the balancer | ignored if the provider does not support it | | `externalTrafficPolicy` | how nodes forward traffic arriving from outside | implemented by the node dataplane | | `allocateLoadBalancerNodePorts` | whether NodePorts are auto-assigned | universal | | `loadBalancerClass` | which implementation should handle this Service | universal | | `loadBalancerIP` | a requested address | **deprecated**: under-specified, non-portable, no dual-stack | Everything else about the balancer is specific to one implementation. ## Annotations as the configuration surface Each provider's cloud-controller-manager reads **annotations** on the Service when it builds the balancer. Typical things they control: - **internal versus internet-facing**: the provider's internal-load-balancer annotation, the usual answer to "make it private" - the **balancer flavour** or tier the provider offers - **TLS**: a certificate reference and which ports terminate TLS - **timeouts, health-check paths and intervals**, and cross-zone behaviour - **address selection**: the implementation's own annotation for a fixed IP, which replaces `loadBalancerIP` (MetalLB, for example, reads `metallb.io/loadBalancerIPs`) The exact keys and values belong to each provider's documentation. The Kubernetes-level facts are these: 1. **No schema validation.** The API server stores any annotation. A misspelled key is accepted, and the provider typically just ignores it. 2. **Not every change is live.** Some providers apply certain annotations only when the balancer is created, so changing them may require recreating the Service, which gives it a new address. 3. **Errors surface as events.** A bad value usually appears as a `SyncLoadBalancerFailed` Warning event on the Service. 4. **Annotations are tenant-writable.** Anyone who can edit the Service can turn an internal balancer into a public one. On a 210-node multi-tenant platform cluster, that fourth point is a governance problem. Platform teams usually run an admission policy engine that restricts which load-balancer annotations each namespace may set, for example requiring the internal annotation everywhere except in approved edge namespaces. ## loadBalancerClass: choosing the implementation `spec.loadBalancerClass` is a label-style string, such as `internal-vip` or `example.com/internal-vip`. Unprefixed names are reserved for end users. - **Not set**: the default implementation (normally the CCM's service controller) handles the Service. - **Set**: the default implementation must **ignore** the Service. Only a controller watching that exact class should act. MetalLB, for example, can be limited to one class with its `--lb-class` flag. - It can only be set when you **create** the Service or **change its type to** LoadBalancer. After that it **cannot be changed**, and it is wiped if the type changes to something else. - If no controller watches the class, EXTERNAL-IP stays `<pending>` and no events appear. This lets one cluster run the provider's balancer for public traffic alongside an in-cluster or appliance-backed implementation for private addresses. ```yaml apiVersion: v1 kind: Service metadata: name: invoice-renderer-private namespace: billing-invoices spec: type: LoadBalancer loadBalancerClass: example.com/internal-vip loadBalancerSourceRanges: - 10.42.16.0/20 selector: app: invoice-renderer ports: - port: 443 targetPort: 8443 ``` ## The cost of one balancer per Service Each LoadBalancer Service normally becomes **a separate cloud balancer**, and each carries its own charges, address and provider quota. On the multi-tenant cluster, 57 tenant Services asking for one each means 57 balancers, each tracking a large node set. The usual pattern: - HTTP(S) services share **one balancer in front of an ingress or gateway controller**. How requests are then routed is that layer's subject. - Dedicated LoadBalancer Services are kept for non-HTTP protocols, strict isolation, or special network placement. - A ResourceQuota on `services.loadbalancers` caps how many each namespace can create.

  • Why was spec.loadBalancerIP deprecated, and what replaces it?
    Its meaning varied between implementations. Some honoured it, some ignored it, and it holds only one address, so it cannot express dual-stack. Kubernetes marked it deprecated and recommends implementation-specific annotations instead, such as MetalLB's `metallb.io/loadBalancerIPs`, which takes a list of addresses. The field still exists in the API, but new manifests should not rely on it.
  • How would you stop a tenant on a shared cluster from turning an internal balancer into a public one?
    Annotations are ordinary metadata, so RBAC cannot restrict individual keys. Use admission control: a validating policy that rejects LoadBalancer Services in tenant namespaces unless they carry the provider's internal annotation or an approved `loadBalancerClass`. Add a `services.loadbalancers` ResourceQuota to cap the count.
  • What does loadBalancerSourceRanges do, and why is it not a complete firewall?
    It lists client CIDRs the implementation should allow through the balancer. It depends on the implementation: the API comment says it is ignored where the provider does not support it. Treat it as one layer. Also check what the provider actually enforced, and still restrict NodePort exposure and in-cluster access separately.

saying these in an interview costs you the question

  • The API server validates provider annotation keys
  • internalTrafficPolicy Local makes the cloud balancer internal
  • loadBalancerClass can be edited later to move implementations
  • loadBalancerSourceRanges is enforced by kube-proxy regardless of provider
  • loadBalancerIP is the portable way to pin an address