# Distribute traffic across clusters with F5 BIG-IP Type of document: How-to guide Product: FABRIC > Configure an ExternalLoadBalancer so F5 BIG-IP acts as the external load balancer for Gateways in two clusters, terminating and re-encrypting TLS and distributing traffic between them. --- This guide describes how to use an F5 BIG-IP system as the external load balancer for NGINX Gateway Fabric Gateways in two clusters, with TLS termination and traffic distribution between them. ## Overview In this guide, you configure an `ExternalLoadBalancer` resource that puts BIG-IP in front of Gateways in two clusters. BIG-IP terminates client TLS and re-encrypts toward NGINX, runs your health monitors and iRules, and spreads traffic across both clusters. The intended use case is a single hostname and certificate served by backends in more than one cluster, such as an active-active deployment or a migration between clusters. Clients see one address, and traffic moves between clusters without a DNS change. See [How configuration reaches BIG-IP](/ngf/external-loadbalancers/gateway-link/quickstart.md#how-configuration-reaches-big-ip). ## Before you begin You need: - Two Kubernetes clusters, referred to in this guide as cluster A and cluster B. - An F5 BIG-IP system running version or later, and an account on it with administrator privileges. - Network access from cluster A to the BIG-IP system, and from BIG-IP to the nodes of both clusters. - Python 3.14 or later. Both clusters run NGINX Gateway Fabric and serve traffic. Cluster A also runs F5 Container Ingress Services, which owns the BIG-IP configuration and reaches cluster B over a kubeconfig, so every step that touches BIG-IP is run against cluster A. This guide installs the AS3 extension, F5 Container Ingress Services, NGINX Gateway Fabric, and cert-manager. cert-manager issues the certificate the Gateway presents on its HTTPS listener, and is installed in both clusters. The shell commands in this guide read the following environment variables, so set them once in the shell you work from and the commands can be copied as they appear: ```shell export BIGIP_ADDRESS="192.0.2.10:443" export BIGIP_USERNAME="admin" export BIGIP_PASSWORD="" export VIRTUAL_SERVER_ADDRESS="192.0.2.100" ``` - `BIGIP_ADDRESS` is the BIG-IP management address, including the port. BIG-IP listens on 443 by default. - `BIGIP_USERNAME` and `BIGIP_PASSWORD` are your BIG-IP credentials. - `VIRTUAL_SERVER_ADDRESS` is a free IPv4 address on the BIG-IP subnet, which BIG-IP listens on. `NGINX_POD_NAME` is set later, and is the name of an NGINX Pod in the cluster you are reading logs from. ## Prepare BIG-IP In this section you install the AS3 extension and create the BIG-IP objects this guide depends on: a partition for F5 Container Ingress Services to own, an iRule, and the SSL profiles and health monitors the `ExternalLoadBalancer` refers to by path. ### AS3 extension F5 Container Ingress Services configures BIG-IP by posting AS3 declarations, so AS3 must be installed before anything else. Follow [Downloading and installing the BIG-IP AS3 package](https://clouddocs.f5.com/products/extensions/f5-appsvcs-extension/latest/userguide/installation.html) in the F5 documentation, then return here. ### Partition Create a partition named `k8s` for F5 Container Ingress Services to own: ```shell curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" -X POST "https://$BIGIP_ADDRESS/mgmt/tm/auth/partition" \ -H "Content-Type: application/json" -d '{"name":"k8s"}' ``` The response describes the new partition: ```json { "name": "k8s", "fullPath": "k8s", "defaultRouteDomain": 0 } ``` F5 Container Ingress Services manages the full contents of its partition. The partition cannot be `Common`, because Container Ingress Services must not modify shared configuration. ### SSL Profiles This guide uses the two SSL profiles that ship with BIG-IP: - `/Common/clientssl` carries the certificate BIG-IP presents to clients, and terminates their TLS connections. - `/Common/serverssl` re-encrypts traffic on the connection BIG-IP opens to NGINX. It does not validate the backend certificate, so the self-signed certificate the Gateway presents is accepted. Both are suitable for testing. In production, replace them with profiles carrying your own certificates, and configure peer verification on the server SSL profile if the backend certificate must be validated. ### HTTP iRules Create an iRule named `gatewaylink_irule`, which inserts a response header: ```text when HTTP_RESPONSE { HTTP::header insert "X-GatewayLink" "true" } ``` The iRule runs on every HTTP response BIG-IP sends back to a client and adds an `X-GatewayLink: true` header to it. Only a Layer 7 virtual server runs HTTP-event iRules, so the header appearing in a response confirms both that BIG-IP built a Layer 7 virtual server and that the iRule is attached to it. You check for the header in [Verify the configuration](#verify-the-configuration). To create the iRule run the following command: ```shell curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" -X POST "https://$BIGIP_ADDRESS/mgmt/tm/ltm/rule" \ -H "Content-Type: application/json" -d '{ "name": "gatewaylink_irule", "apiAnonymous": "when HTTP_RESPONSE { HTTP::header insert \"X-GatewayLink\" \"true\" }" }' ``` ### Health Monitors This guide uses two health monitors that ship with BIG-IP, so there is nothing to create: - `/Common/http` checks the HTTP pool members by sending a request and waiting for a response. - `/Common/tcp` checks the HTTPS pool members by opening a TCP connection, without inspecting encrypted traffic. BIG-IP marks a pool member offline when its monitor fails and stops sending traffic to it, so each virtual server is checked in a way that suits the traffic it carries. ## Connect cluster A to cluster B F5 Container Ingress Services runs in cluster A and reaches cluster B over a kubeconfig. Build that kubeconfig, because Container Ingress Services needs it at install time. On **cluster B**, grant F5 Container Ingress Services read access: ```yaml kubectl apply -f - < remote-kubeconfig.yaml </` form. F5 Container Ingress Services reads the list of external clusters and their kubeconfig Secrets from it, so without this value it has no way to reach cluster B. - `args.pool_member_type` must match the type of the Gateway's Service. Use `nodeport` with `NodePort`, or `cluster` with `ClusterIP`. - `args.log-as3-response=true` logs the BIG-IP response to each declaration, which is useful for troubleshooting. Confirm F5 Container Ingress Services reached BIG-IP and accepted the mode: ```shell kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr | grep -E "authn/login|multi-cluster-mode" ``` The log shows a successful login and the configured multi-cluster mode: ```text [DEBUG] [BIGIP] postConfig request: POST https://192.0.2.10:443/mgmt/shared/authn/login 200 OK [DEBUG] Multi-cluster-mode: standalone, local cluster name: local ``` ## Set up for both clusters Apply the following resources to **both** clusters. Use the same Gateway name and the same listeners in each, so the data plane Services carry matching labels and expose the same ports. F5 Container Ingress Services builds one virtual server per Service port and pools every cluster behind that virtual server. ### Install the custom resource definitions Install the F5 Container Ingress Services custom resource definitions in **both** clusters, including cluster B, which does not run F5 Container Ingress Services: ```shell kubectl apply -f https://raw.githubusercontent.com/F5Networks/k8s-bigip-ctlr/v/docs/config_examples/customResourceDefinitions/customresourcedefinitions.yml ``` Confirm the `IngressLink` custom resource definition is installed: ```shell kubectl get crd ingresslinks.cis.f5.com ``` ```text NAME CREATED AT ingresslinks.cis.f5.com 2026-08-05T01:40:54Z ``` With external load balancer support enabled, NGINX Gateway Fabric watches `IngressLink` resources on startup in every cluster it runs in. A cluster without the custom resource definition leaves the control plane unable to start, and its Pod restarts continuously: ```text no matches for kind "IngressLink" in version "cis.f5.com/v1" failed to start control loop: failed to wait for provisioner-IngressLink caches to sync ``` ### Install NGINX Gateway Fabric [Install](/ngf/install/) NGINX Gateway Fabric with external load balancer support enabled. Using Helm, set the `nginxGateway.externalLoadBalancer.enable=true` value. Using Kubernetes manifests, add the `--external-load-balancer` flag to the `nginx-gateway` container arguments. ### Create a Gateway Create an `NginxProxy` resource named `gatewaylink-proxy`, which exposes the readiness probe: ```yaml kubectl apply -f - <", v.get("rules", "no rules"))' ``` If the iRule is attached, confirm the trusted addresses: ```shell kubectl exec $NGINX_POD_NAME -c nginx -- grep set_real_ip_from /etc/nginx/conf.d/http.conf ``` Set `trustedAddresses` on the `NginxProxy` resource to the subnet of the IP address which the BIG-IP system uses to send traffic to NGINX. ### A configured field has no effect Kubernetes discards fields that are not in the installed custom resource definition schema without reporting an error, so both controllers report success while the field never arrives. Check where the field stops: ```shell export FIELD_NAME="ipamLabel" kubectl get crd ingresslinks.cis.f5.com -o yaml | grep -A5 "$FIELD_NAME" kubectl logs -n nginx-gateway deploy/ngf-nginx-gateway-fabric | grep "unknown field" kubectl get ingresslink gateway-nginx -o jsonpath='{.spec}' | python3 -m json.tool ``` An `unknown field` message means the installed custom resource definition is older than the NGINX Gateway Fabric release. Install a matching version. ### The control plane restarts continuously in cluster B The NGINX Gateway Fabric Pod reports `CrashLoopBackOff`, and its logs end with a cache sync failure: ```text no matches for kind "IngressLink" in version "cis.f5.com/v1" failed to start control loop: failed to wait for provisioner-IngressLink caches to sync ``` The F5 Container Ingress Services custom resource definitions are missing from that cluster. With external load balancer support enabled, NGINX Gateway Fabric watches `IngressLink` resources on startup, whether or not Container Ingress Services runs there. - Install the custom resource definitions in the affected cluster: ```shell kubectl apply -f https://raw.githubusercontent.com/F5Networks/k8s-bigip-ctlr/v/docs/config_examples/customResourceDefinitions/customresourcedefinitions.yml ``` - Delete the Pod so it restarts immediately rather than waiting out its backoff: ```shell kubectl delete pod -n nginx-gateway -l app.kubernetes.io/name=nginx-gateway-fabric ``` ### The remote cluster has no pool Only `_local` pools exist on BIG-IP, and traffic never reaches cluster B. F5 Container Ingress Services could not load the cluster B kubeconfig, so it has no endpoints to pool. Start with its log, which names the cause directly: ```shell kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr | grep -i "MultiCluster" ``` - Confirm the Secret exists under the name and namespace the `extended-spec-config` ConfigMap refers to. A Secret created under a different name is reported as missing: ```text error occurred while fetching Secret: remote-kubeconfig for the cluster: remote, Error: secrets "remote-kubeconfig" not found ``` - Confirm the token is still valid. A token issued for a ServiceAccount that has since been deleted and recreated is rejected: ```text the server has asked for the client to provide credentials ``` Regenerate the kubeconfig on cluster B and recreate the Secret. - Confirm the Secret holding the cluster B kubeconfig parses. A kubeconfig with broken indentation is stored without complaint and fails only when F5 Container Ingress Services loads it: ```shell kubectl get secret remote-kubeconfig -n kube-system -o jsonpath='{.data.kubeconfig}' | base64 -d > /tmp/check.yaml KUBECONFIG=/tmp/check.yaml kubectl get nodes ``` The command lists the cluster B nodes. An error such as `mapping values are not allowed in this context` means the file is malformed, so regenerate it and recreate the Secret. - Confirm the `clusterName` in the extended spec ConfigMap matches the `clusterName` under `remoteClusters` in the `ExternalLoadBalancer`. - Restart F5 Container Ingress Services after replacing the Secret, because it reads the kubeconfig at startup: ```shell kubectl rollout restart deploy/f5-cis-f5-bigip-ctlr -n kube-system ``` ### The remote pool member is down The `_remote` pool exists but its member reports `offline`, so all traffic goes to the local cluster. - Confirm the cluster B data plane Service exposes the same ports as cluster A. A missing HTTPS listener leaves nothing listening on the 443 NodePort: ```shell kubectl get svc gateway-nginx -o jsonpath='{range .spec.ports[*]}{.name} {.port}:{.nodePort}{"\n"}{end}' ``` - Confirm the `nginx-tls` Secret exists in cluster B. Without it the HTTPS listener is not programmed and NGINX never listens on 443. - Restart the data plane after creating a certificate. NGINX does not load a certificate created after the Pod started, and the Gateway reports every condition as healthy while the listener is missing from the configuration: ```shell kubectl rollout restart deploy/gateway-nginx -n default ``` ### Requests fail with a connection reset A request through BIG-IP fails with `Recv failure: Connection reset by peer`. NGINX Gateway Fabric enables HTTP/2 by default. BIG-IP SSL profiles do not negotiate HTTP/2 unless configured to, so BIG-IP sends HTTP/1.1 into a connection NGINX set up for HTTP/2. - Set `disableHTTP2: true` on the `NginxProxy` resource, or use a BIG-IP SSL profile with HTTP/2 enabled. Confirm the setting reached the data plane rather than trusting the resource: ```shell kubectl exec $NGINX_POD_NAME -c nginx -- grep "listen 443" /etc/nginx/conf.d/http.conf ``` The absence of an `http2` token on the `listen` line means HTTP/2 is off. ## Remove the configuration Delete the `ExternalLoadBalancer` so F5 Container Ingress Services deletes the objects it created on BIG-IP: ```shell kubectl delete externalloadbalancer gateway-elb ``` Confirm the virtual servers are gone: ```shell curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" "https://$BIGIP_ADDRESS/mgmt/tm/ltm/virtual" | python3 -m json.tool | grep fullPath ``` ## References - [F5 IngressLink documentation](https://clouddocs.f5.com/containers/latest/userguide/ingresslink/): the F5 Container Ingress Services resource that NGINX Gateway Fabric generates. - [F5 Application Services 3 Extension reference](https://clouddocs.f5.com/products/extensions/f5-appsvcs-extension/latest/refguide/schema-reference.html): the declaration format F5 Container Ingress Services posts to BIG-IP. - [F5 Container Ingress Services](https://github.com/F5Networks/k8s-bigip-ctlr): the F5 Container Ingress Services source and custom resource definitions. - [F5 IPAM Controller](https://github.com/F5Networks/f5-ipam-controller): allocates virtual server addresses when using the `ipamLabel` field instead of a fixed address. - [F5 BIG-IP iControl REST API](https://clouddocs.f5.com/api/icontrol-rest/): the API used by the `curl` commands in this guide. - [BIG-IP Virtual Edition on Amazon Web Services](https://clouddocs.f5.com/cloud/public/v1/aws_index.html) - [BIG-IP Virtual Edition on Microsoft Azure](https://clouddocs.f5.com/cloud/public/v1/azure_index.html) - [BIG-IP Virtual Edition on Google Cloud Platform](https://clouddocs.f5.com/cloud/public/v1/google_index.html) - [F5 Container Ingress Services multi-cluster guide](https://clouddocs.f5.com/containers/latest/userguide/multicluster/): multi-cluster deployment topologies.