GKE
This guide shows how to run Service LoadBalancer Multiplexer on Google Kubernetes Engine (GKE) using GKE-managed LoadBalancer Services.
The default chart values target an external GKE passthrough Network Load Balancer in backend service mode. The normal operating model does not require the controller to create Google Cloud firewall rules, forwarding rules, or other cloud resources directly.
Architecture On GKE
Section titled “Architecture On GKE”A GKE install has two Service roles:
- Mux Service: selectorless
type: LoadBalancer; GKE creates the cloud load balancer for this Service. - Channel Service: application-facing
type: LoadBalancer; points at the mux throughspec.loadBalancerClass.
Traffic flow:
client -> GKE external passthrough Network Load Balancer -> mux Service port -> controller-managed mux Endpoints -> channel backend podsControl flow:
channel Service + channel Endpoints -> svc-lb-mux controller -> mux Service ports + mux Endpoints -> channel status.loadBalancer.ingressThe mux Service has no selector. The controller owns the mux runtime ports and Endpoints. When no channels exist, the controller keeps a placeholder 101/TCP port so Kubernetes accepts the Service.
Recommended Defaults
Section titled “Recommended Defaults”The chart defaults are GKE-oriented:
defaultLoadBalancer: create: true name: mux annotations: cloud.google.com/l4-rbs: "enabled" loadBalancerClass: "" allocateLoadBalancerNodePorts: true portRange: "20000-20099" maxPorts: 100 allocationConfigMapName: ""cloud.google.com/l4-rbs: "enabled" asks GKE to create a backend service-based external passthrough Network Load Balancer. Keep this default for GKE unless you have a provider-specific reason to use the older target pool path.
Forwarding rule port behavior is managed by GKE from the mux Service port list:
- In backend service mode, GKE can publish a small number of active mux ports as a discrete
portslist. - When the active mux ports exceed that discrete-port shape, GKE can switch the forwarding rule to a contiguous
portRangespanning the smallest and largest mux ports. - In live GKE testing, four ports
10301,10302,20301, and20302rendered as discrete ports. Adding two more non-contiguous ports changed the forwarding rule toportRange: 10301-20302. - Target pool-based external load balancers also use a contiguous forwarding rule port range.
Both modes can work for mux traffic, but wide gaps between channel ports can reserve a broad forwarding rule range, and GKE may not immediately shrink that range after channels are removed. Plan channel external ports in compact ranges when possible.
Install with defaults:
helm install svc-mux oci://ghcr.io/nowakeai/charts/svc-lb-mux \ --version 0.1.1 \ --namespace svc-mux \ --create-namespace
kubectl get svc mux -n svc-mux -wThe names svc-mux and mux are defaults only. Use names that match your namespace, product, or ownership model.
Static External IP
Section titled “Static External IP”For production, reserve a regional static external IPv4 address in the same region as the GKE cluster.
PROJECT_ID=my-projectREGION=us-central1ADDRESS_NAME=svc-mux-ip
gcloud compute addresses create $ADDRESS_NAME \ --project $PROJECT_ID \ --region $REGION
MUX_IP=$(gcloud compute addresses describe $ADDRESS_NAME \ --project $PROJECT_ID \ --region $REGION \ --format='value(address)')There are two common binding styles.
Bind By IP Address
Section titled “Bind By IP Address”defaultLoadBalancer: loadBalancerIP: "203.0.113.10"helm upgrade --install svc-mux oci://ghcr.io/nowakeai/charts/svc-lb-mux \ --version 0.1.1 \ --namespace svc-mux \ --create-namespace \ --set defaultLoadBalancer.loadBalancerIP=$MUX_IPBind By Address Resource Name
Section titled “Bind By Address Resource Name”Use the GKE address-name annotation when you want manifests to reference the reserved address resource instead of embedding the numeric IP.
defaultLoadBalancer: annotations: cloud.google.com/l4-rbs: "enabled" networking.gke.io/load-balancer-ip-addresses: svc-mux-ip loadBalancerClass: networking.gke.io/l4-regional-externalhelm upgrade --install svc-mux oci://ghcr.io/nowakeai/charts/svc-lb-mux \ --version 0.1.1 \ --namespace svc-mux \ --create-namespace \ --set-string defaultLoadBalancer.annotations.networking\.gke\.io/load-balancer-ip-addresses=$ADDRESS_NAME \ --set defaultLoadBalancer.loadBalancerClass=networking.gke.io/l4-regional-externalSet loadBalancerClass before creating the Service. Kubernetes treats Service load balancer class as immutable.
Channel Services
Section titled “Channel Services”A channel Service points at the mux through spec.loadBalancerClass:
apiVersion: v1kind: Servicemetadata: name: my-service namespace: my-namespacespec: type: LoadBalancer loadBalancerClass: svc-mux.nowake.ai/mux.svc-mux allocateLoadBalancerNodePorts: false selector: app: my-app ports: - name: http port: 80 targetPort: 8080Channel rules:
- Use
<api-prefix>/<mux>[.<namespace>]forloadBalancerClass. - Name every port; port names are used as stable mapping identity.
- Set
allocateLoadBalancerNodePorts: falseunless a provider-specific workflow requires NodePorts. - Each
(external port, protocol)pair can be claimed by only one channel on the same mux.
By default, the controller uses spec.ports[].port as the external mux port.
If the desired public mux port is already in spec.ports[].port, no
external-ports annotation is needed. For the full port model, see
Channel Service manual.
To override the external port:
metadata: annotations: svc-mux.nowake.ai/external-ports: "http:8080"To allocate automatically from the mux port range:
metadata: annotations: svc-mux.nowake.ai/external-ports: "http:auto"Automatic assignments and static port claims are stored in one state ConfigMap per mux. Do not reuse one state ConfigMap across multiple muxes.
GKE Port And Firewall Model
Section titled “GKE Port And Firewall Model”The controller deliberately stays inside the GKE Service LoadBalancer model:
- GKE owns forwarding rules, backend services, health checks, NEGs, and firewall rules.
- The controller does not need Google Cloud IAM permissions in the normal path.
- One GKE mux is limited to 100 Service ports.
- Larger workloads should be split across multiple mux Services.
The default GKE range matches the GKE mux limit:
defaultLoadBalancer: portRange: "20000-20099" maxPorts: 100If the controller detects a GKE-backed mux without svc-mux.nowake.ai/max-ports, it applies the GKE limit of 100 ports and emits GkePortLimitApplied. If a detected GKE mux configures a higher value, the controller caps the effective value to 100 and emits the same Warning event.
When a new channel would exceed the effective mux limit, the controller skips that channel and emits MuxPortLimitExceeded.
Do not manually edit GKE-managed firewall rules. If more than 100 one-port channels are needed, create additional muxes with non-overlapping ranges, for example:
mux-a: 20000-20099mux-b: 20100-20199mux-c: 20200-20299Internal Load Balancer
Section titled “Internal Load Balancer”For an internal GKE passthrough Network Load Balancer, use GKE’s internal load balancer annotation and remove the external RBS annotation:
defaultLoadBalancer: annotations: cloud.google.com/l4-rbs: "" networking.gke.io/load-balancer-type: Internal loadBalancerClass: ""Choose internal versus external mode before production rollout because several Service load balancer parameters are immutable after creation.
Capacity Planning
Section titled “Capacity Planning”For one-port channels, the mux capacity is bounded by the smallest of:
configured port range sizeGKE Service LoadBalancer 100-port limitKubernetes Endpoints object sizeGoogle Cloud regional quotasWith the default range:
20099 - 20000 + 1 = 100 portsSo one GKE mux supports up to 100 one-port channel mappings in the current implementation.
A mux also consumes Google Cloud resources. Check regional/project quota for:
- forwarding rules
- external IPv4 addresses, if using reserved static IPs
- backend services
- health checks
- firewall rules
gcloud compute project-info describe \ --format="table(quotas.metric,quotas.limit,quotas.usage)"The current controller writes a legacy Endpoints object for the mux. Endpoint data grows with channel count and backend replica count:
endpoint_entries ~= channel_count * ready_backend_endpoints_per_channelFor high channel counts or large replica sets, split channels across muxes until EndpointSlice support lands.
Validate Generated GCP Resources
Section titled “Validate Generated GCP Resources”Inspect Kubernetes state:
kubectl get svc mux -n svc-mux -o widekubectl get endpoints mux -n svc-mux -o yamlkubectl get configmap mux-port-allocations -n svc-mux -o yamlkubectl get events -n svc-mux --sort-by=.lastTimestampInspect Google Cloud resources:
gcloud compute forwarding-rules list \ --regions $REGION \ --filter="IPAddress=$MUX_IP"
gcloud compute backend-services list \ --regions $REGION
gcloud compute firewall-rules list \ --filter="destinationRanges:$MUX_IP"A validated GKE external mux should look like:
reserved static IP -> regional forwarding rule, TCP, portRange 20000-20099 -> regional backend service, protocol TCP -> GCE_VM_IP NEG containing GKE nodes -> GKE-managed firewall rule allowing mux Service portsPublic traffic should be tested against the mux external IP and every allocated channel port. The repository includes a pressure-test helper for 100 channels backed by 100 distinct pods:
test-local/gke-pressure.py manifest \ --namespace svc-mux-eip-test \ --load-balancer-class svc-mux.nowake.ai/mux-eip.svc-mux-eip \ | kubectl apply -f -
kubectl get configmap mux-eip-port-allocations \ -n svc-mux-eip \ -o jsonpath='{.data.allocations\.json}' > /tmp/mux-eip-allocations.json
test-local/gke-pressure.py probe \ --host 203.0.113.10 \ --allocations-json /tmp/mux-eip-allocations.json \ --namespace svc-mux-eip-testA successful probe reports:
ok=100 missing=0 failed=0See gke-pressure-test-report.md for a sanitized validation report.
Troubleshooting
Section titled “Troubleshooting”Common checks:
- Static IP is regional and in the same region as the cluster.
loadBalancerClasswas set before Service creation.- Channel ports are named.
- Channel Services use
allocateLoadBalancerNodePorts: falseunless NodePorts are intentionally required. - GitOps ignores mux
spec.portsfor controller-managed mux Services. - GKE-managed forwarding rule and firewall rule cover the mux port range.
MuxPortConflict,MuxPortLimitExceeded, andInvalidPortMappingevents explain rejected channels.