Use F5 BIG-IP as an external load balancer
This guide describes how to use an F5 BIG-IP system as the external load balancer for an NGINX Gateway Fabric Gateway.
GatewayLink integrates NGINX Gateway Fabric with F5 BIG-IP Container Ingress Services to configure an F5 BIG-IP system as the external load balancer for a Gateway. You describe the desired BIG-IP configuration through the ExternalLoadBalancer custom resource.
In this guide, the F5 IPAM Controller allocates the address that BIG-IP listens on, and an iRule preserves the original client address by forwarding it to NGINX using the PROXY protocol.
NGINX Gateway Fabric watches ExternalLoadBalancer resources. For each one, it creates an IngressLink resource, the custom resource F5 Container Ingress Services uses to describe a Gateway that BIG-IP fronts. The IngressLink carries the settings from the ExternalLoadBalancer spec, along with a label selector that matches the Gateway’s data plane Service.
F5 Container Ingress Services watches IngressLink resources. It resolves the selector to the data plane Service, reads its node addresses and NodePorts, and compiles them into an AS3 declaration. It posts that declaration to the AS3 endpoint on BIG-IP, which creates the virtual server and its pool. F5 Container Ingress Services reposts the declaration whenever the endpoints or the IngressLink change, so BIG-IP stays current as Pods come and go.
flowchart LR
A[ExternalLoadBalancer
NGINX Gateway Fabric] --> B[IngressLink
F5 Container Ingress Services]
B --> C[AS3 declaration
POST to BIG-IP]
C --> D[BIG-IP
virtual server, pool]
You need:
- A Kubernetes cluster.
- An F5 BIG-IP system running version 17.1.0.3 or later, and an account on it with administrator privileges.
- Network access from the cluster to the BIG-IP system, and from BIG-IP to the cluster node addresses.
- Python 3.14 or later.
This guide installs the AS3 extension, the F5 IPAM Controller, F5 Container Ingress Services, and NGINX Gateway Fabric.
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:
export BIGIP_ADDRESS="192.0.2.10:443"
export BIGIP_USERNAME="admin"
export BIGIP_PASSWORD="<your-password>"
export IPAM_ADDRESS_RANGE="192.0.2.100-192.0.2.110"BIGIP_ADDRESSis the BIG-IP management address, including the port. BIG-IP listens on 443 by default.BIGIP_USERNAMEandBIGIP_PASSWORDare your BIG-IP credentials.IPAM_ADDRESS_RANGEis a free address range on the BIG-IP subnet, which the F5 IPAM Controller allocates from. You choose this range in Install the F5 IPAM Controller.
Two more variables are set later, once their values exist:
ALLOCATED_ADDRESSis the virtual server address the F5 IPAM Controller allocates, read from theIngressLinkstatus in Verify the configuration.NGINX_POD_NAMEis the name of an NGINX Pod, used when reading its logs.
In this section you install the AS3 extension and create the two BIG-IP objects this guide depends on: a partition for F5 Container Ingress Services to own, and an iRule that adds a PROXY protocol header.
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 in the F5 documentation, then return here.
Create a partition named k8s for F5 Container Ingress Services to own:
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:
{
"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.
This guide uses a TCP iRule named Proxy_Protocol_iRule:
when SERVER_CONNECTED {
TCP::respond "PROXY TCP[IP::version] [IP::client_addr] [clientside {IP::local_addr}] [TCP::client_port] [clientside {TCP::local_port}]\r\n"
}The iRule runs on the SERVER_CONNECTED event, which fires when BIG-IP opens a connection to NGINX, before any application data is sent. It writes a single PROXY protocol header onto that connection. The header carries the original client address, so NGINX can report it instead of the BIG-IP self-IP address.
To create the iRule:
curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" -X POST "https://$BIGIP_ADDRESS/mgmt/tm/ltm/rule" \
-H "Content-Type: application/json" -d '{
"name": "Proxy_Protocol_iRule",
"apiAnonymous": "when SERVER_CONNECTED {\n TCP::respond \"PROXY TCP[IP::version] [IP::client_addr] [clientside {IP::local_addr}] [TCP::client_port] [clientside {TCP::local_port}]\\r\\n\"\n}"
}'The response describes the new iRule:
{
"name": "Proxy_Protocol_iRule",
"fullPath": "/Common/Proxy_Protocol_iRule",
"apiAnonymous": "when SERVER_CONNECTED { ... }"
}The F5 IPAM Controller allocates the virtual server address from a range you define, so you do not have to pick and track an address by hand.
Allocation is a handoff between the two controllers through a shared IPAM resource. F5 Container Ingress Services creates that resource on startup when it is installed with --ipam=true. When an IngressLink names an IPAM label, Container Ingress Services adds an entry to the resource spec requesting an address under that label. The F5 IPAM Controller watches the same resource, takes an address from the range configured for that label, and records the assignment in the resource status. Container Ingress Services reads the address from the status and uses it as the virtual server address in the AS3 declaration.
Install the F5 IPAM Controller before Container Ingress Services, so it is watching by the time the first request is made.
Install the IPAM custom resource definition:
kubectl apply -f - <<EOF
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: ipams.fic.f5.com
spec:
group: fic.f5.com
names:
kind: IPAM
listKind: IPAMList
plural: ipams
singular: ipam
scope: Namespaced
versions:
- name: v1
served: true
storage: true
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
properties:
spec:
type: object
x-kubernetes-preserve-unknown-fields: true
status:
type: object
x-kubernetes-preserve-unknown-fields: true
EOFThen install the controller itself:
Choose the address range the F5 IPAM Controller allocates from.
The range must be addresses on the same subnet as the BIG-IP self-IP, and not in use by anything else. To Find the subnet, run the following command:
curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" "https://$BIGIP_ADDRESS/mgmt/tm/net/self" \
| python3 -c 'import sys,json;[print(x["name"],x["address"]) for x in json.load(sys.stdin)["items"]]'The output reports each self-IP with its prefix length:
self_1nic 192.0.2.10/24A self-IP of 192.0.2.10/24 means the allocated address must fall within 192.0.2.1 to 192.0.2.254. Set IPAM_ADDRESS_RANGE to a free range inside that subnet.
Add the F5 IPAM Controller Helm repository:
helm repo add f5-ipam-stable https://f5networks.github.io/f5-ipam-controller/helm-charts/stable --force-update
helm repo updateInstall the F5 IPAM Controller with an address range:
helm install f5-ipam-controller f5-ipam-stable/f5-ipam-controller \
--namespace kube-system \
--set image.version=0.1.13 \
--set namespace=kube-system \
--set rbac.create=true \
--set serviceAccount.create=true \
--set args.log_level=DEBUG \
--set pvc.create=true \
--set pvc.storage=100Mi \
--set-string 'args.ip_range=\{"production":"'"$IPAM_ADDRESS_RANGE"'"\}' \
--waitThe args.ip_range value maps a pool name to a range of addresses. The pool name production is what the ExternalLoadBalancer refers to later through its ipamLabel field.
Confirm the F5 IPAM Controller is running:
kubectl get pods -n kube-system -l app=f5-ipam-controllerNAME READY STATUS RESTARTS AGE
f5-ipam-controller-79448b4b8f-qmws7 1/1 Running 0 20sInstall the F5 Container Ingress Services custom resource definitions:
kubectl apply -f https://raw.githubusercontent.com/F5Networks/k8s-bigip-ctlr/v2.20.4/docs/config_examples/customResourceDefinitions/customresourcedefinitions.ymlConfirm the IngressLink custom resource definition is installed:
kubectl get crd ingresslinks.cis.f5.comNAME CREATED AT
ingresslinks.cis.f5.com 2026-08-05T01:40:54ZDeploy F5 Container Ingress Services:
helm repo add f5-stable https://f5networks.github.io/charts/stable
helm repo update
helm install f5-cis f5-stable/f5-bigip-ctlr -n kube-system \
--set bigip_secret.create=true \
--set bigip_secret.username="$BIGIP_USERNAME" \
--set bigip_secret.password="$BIGIP_PASSWORD" \
--set rbac.create=true \
--set serviceAccount.create=true \
--set namespace=kube-system \
--set args.bigip_url="$BIGIP_ADDRESS" \
--set args.bigip_partition=k8s \
--set args.pool_member_type=nodeport \
--set args.custom_resource_mode=true \
--set args.insecure=true \
--set args.log_level=DEBUG \
--set args.log-as3-response=true \
--set args.ipam=trueThese fields must be set according to your own setup:
args.bigip_urlmust include the port. F5 Container Ingress Services assumes 443, so omitting a non-default port causes connection failures.args.custom_resource_mode=trueis required. Without it, F5 Container Ingress Services never watchesIngressLinkresources.args.pool_member_typemust match the type of the Gateway’s Service. UsenodeportwithNodePort, orclusterwithClusterIP.args.ipam=trueis required for the F5 IPAM Controller to allocate the virtual server address.args.log-as3-response=truelogs the BIG-IP response to each declaration, which is useful for troubleshooting.
Confirm F5 Container Ingress Services reached BIG-IP:
kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr | grep "authn/login"A successful login is logged as a 200 response.
2026/08/05 14:27:22 [DEBUG] [2026-08-05 14:27:22,539 urllib3.connectionpool DEBUG] https://192.0.2.10:443 "POST /mgmt/shared/authn/login HTTP/1.1" 200 722
2026/08/05 14:27:23 [DEBUG] [2026-08-05 14:27:23,896 urllib3.connectionpool DEBUG] https://192.0.2.10:443 "POST /mgmt/shared/authn/login HTTP/1.1" 200 722No output at all means Container Ingress Services never attempted a login, so check the logs and the BIG-IP address.
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.
In this section you create a Gateway, a coffee application with an HTTPRoute, and an ExternalLoadBalancer custom resource that puts BIG-IP in front of the Gateway, then send a request through BIG-IP to confirm traffic reaches the application.
Create an NginxProxy resource named gatewaylink-proxy:
kubectl apply -f - <<EOF
apiVersion: gateway.nginx.org/v1alpha2
kind: NginxProxy
metadata:
name: gatewaylink-proxy
spec:
rewriteClientIP:
mode: ProxyProtocol
trustedAddresses:
- type: CIDR
value: 0.0.0.0/0
kubernetes:
service:
type: NodePort
externalTrafficPolicy: Cluster
deployment:
container:
readinessProbe:
expose: true
port: 8081
path: /nginx-ready
EOFThis NginxProxy configures the Gateway that references it with the settings BIG-IP depends on:
rewriteClientIP.mode: ProxyProtocolreads the client address from the PROXY protocol header the iRule adds.service.typesets the type of the Gateway’s Service, and must match the F5 Container Ingress Servicespool_member_type.readinessProbe.exposeputs the readiness port on the Service, andreadinessProbe.pathsets the path the generated health monitor requests. NGINX Gateway Fabric serves its readiness endpoint at/readyzby default, while the monitor generated by F5 Container Ingress Services requests/nginx-ready, so setting the path here makes the two agree.rewriteClientIP.trustedAddresseslists the addresses NGINX accepts a client address from.
ImportantThis example uses
0.0.0.0/0, which trusts every address. NGINX checks this list against the address on the connection and against the client address inside the PROXY protocol header, and discards the header if either is untrusted, leaving an internal address in the log with no error reported.For a narrower list, use the subnet of the IP address which the BIG-IP system uses to send traffic to NGINX.
Create a Gateway named gateway with an HTTP listener:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
spec:
gatewayClassName: nginx
infrastructure:
parametersRef:
name: gatewaylink-proxy
group: gateway.nginx.org
kind: NginxProxy
listeners:
- name: http
port: 80
protocol: HTTP
hostname: cafe.example.com
EOFF5 Container Ingress Services builds one virtual server per Service port.
Confirm the Gateway is Accepted and Programmed:
kubectl describe gateways.gateway.networking.k8s.io gatewayVerify the status is Accepted and Programmed:
Status:
Conditions:
Message: The Gateway is accepted
Reason: Accepted
Status: True
Type: AcceptedCreate the coffee application by copying and pasting the following block into your terminal:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 1
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
EOFCreate an HTTPRoute named coffee that attaches to the Gateway and routes /coffee to that Service:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: coffee
spec:
parentRefs:
- name: gateway
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /coffee
backendRefs:
- name: coffee
port: 80
EOFConfirm the application is running and the route is attached:
kubectl get pods,svc,httprouteThe output lists coffee Pod, the coffee Service, and the HTTPRoute:
NAME READY STATUS RESTARTS AGE
pod/coffee-7dd75bc79b-cqvb7 1/1 Running 0 77s
NAME TYPE CLUSTER-IP PORT(S) AGE
service/coffee ClusterIP 10.43.174.12 80/TCP 77s
NAME HOSTNAMES AGE
httproute.gateway.networking.k8s.io/coffee ["cafe.example.com"] 32sCreate an ExternalLoadBalancer resource named gateway-elb:
kubectl apply -f - <<EOF
apiVersion: gateway.nginx.org/v1alpha1
kind: ExternalLoadBalancer
metadata:
name: gateway-elb
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gateway
gatewayLink:
ipamLabel: "production"
partition: k8s
iRules:
- /Common/Proxy_Protocol_iRule
EOFThe ipamLabel field tells the F5 IPAM Controller which address range to allocate from. The value must match a pool name in the args.ip_range map used when installing the F5 IPAM Controller.
Confirm NGINX Gateway Fabric accepted the resource:
kubectl describe externalloadbalancers.gateway.nginx.org gateway-elbVerify the status is Accepted:
Status:
Controllers:
Conditions:
Last Transition Time: 2026-08-05T02:21:16Z
Message: The ExternalLoadBalancer is accepted
Observed Generation: 1
Reason: Accepted
Status: True
Type: Accepted
Controller Name: gateway.nginx.org/nginx-gateway-controllerConfirm the IngressLink was written and an address was allocated:
kubectl describe ingresslink gateway-nginxStatus:
Last Updated: 2026-08-05T02:23:16Z
Status: OK
Vs Address: 192.0.2.100
Events: <none>F5 Container Ingress Services writes this status after posting the AS3 declaration. A status of OK means BIG-IP accepted the declaration, and vsAddress is the address the F5 IPAM Controller allocated.
Store that address for the remaining commands:
export ALLOCATED_ADDRESS=$(kubectl get ingresslink gateway-nginx -o jsonpath='{.status.vsAddress}')Send a request through BIG-IP:
curl -H "Host: cafe.example.com" http://$ALLOCATED_ADDRESS/coffeeThe request returns 200 OK with a response body from the backend application.
Server address: 10.42.0.43:8080
Server name: coffee-7b9578cff9-t7r7v
Date: 05/Aug/2026:14:33:40 +0000
URI: /coffee
Request ID: a2ae0944885fdf99bb5f86038aeae84fConfirm NGINX sees the original client address:
export NGINX_POD_NAME=$(kubectl get pods -l app.kubernetes.io/name=gateway-nginx -o jsonpath='{.items[0].metadata.name}')
kubectl logs $NGINX_POD_NAME -c nginx | grep coffeeThe access log records the address of the machine you sent the request from.
Confirm the --external-load-balancer flag is set on the control plane deployment. Helm ignores values a chart does not define, so a chart without external load balancer support renders a deployment without the flag:
kubectl get deploy -n nginx-gateway ngf-nginx-gateway-fabric \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="nginx-gateway")].args}'F5 Container Ingress Services writes this status, so an empty status means it has not processed the resource. Wait up to two minutes for reconciliation, then check its logs:
kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlrConfirm F5 Container Ingress Services was deployed with args.ipam=true, then check the F5 IPAM Controller logs:
kubectl logs -n kube-system -l app=f5-ipam-controller --tail=20A label that does not match a configured pool is reported directly:
[PROV] IPAM LABEL: gatewaylink Not FoundSet ipamLabel on the ExternalLoadBalancer to a pool name from the args.ip_range map used when installing the F5 IPAM Controller.
Read the BIG-IP response in the F5 Container Ingress Services logs, which usually names the problem:
kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr | grep -E "AS3\]\[POST\]|response:"The Pod is in CrashLoopBackOff and its logs contain [ERROR] AS3 RPM is not installed on BIGIP. F5 Container Ingress Services infers this from a 404 on the AS3 endpoint, so it also appears when AS3 is installed but not serving. See Troubleshooting in the F5 documentation.
After restoring AS3, delete the Pod so it retries without waiting out its backoff:
kubectl delete pod -n kube-system -l app=f5-cis-f5-bigip-ctlrConfirm the type of the Gateway’s Service matches the F5 Container Ingress Services pool_member_type, and that the Gateway has a listener on the port the pool was built for. A missing or invalid certificateRefs Secret leaves an HTTPS listener unprogrammed, so the Service never exposes port 443:
kubectl get svc gateway-nginx -o jsonpath='{.spec.type}{"\n"}{.spec.ports}'
kubectl describe gateways.gateway.networking.k8s.io gatewayThe client address travels inside the PROXY protocol header. NGINX reads it only when both the connection address and the address inside the header are trusted, so an internal address in the log means the header never arrived or was discarded.
Confirm the iRule is attached. Creating an iRule on BIG-IP does not attach it to anything:
curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" "https://$BIGIP_ADDRESS/mgmt/tm/ltm/virtual" \
| python3 -c 'import sys,json
for v in json.load(sys.stdin)["items"]:
print(v["fullPath"], "->", v.get("rules", "no rules"))'If the iRule is attached, confirm the trusted addresses:
kubectl exec $NGINX_POD_NAME -c nginx -- grep set_real_ip_from /etc/nginx/conf.d/http.confSet trustedAddresses on the NginxProxy resource to the subnet of the IP address which the BIG-IP system uses to send traffic to NGINX.
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:
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.toolAn unknown field message means the installed custom resource definition is older than the NGINX Gateway Fabric release. Install a matching version.
Delete the ExternalLoadBalancer so F5 Container Ingress Services deletes the objects it created on BIG-IP:
kubectl delete externalloadbalancer gateway-elbConfirm the virtual servers are gone:
curl -sku "$BIGIP_USERNAME:$BIGIP_PASSWORD" "https://$BIGIP_ADDRESS/mgmt/tm/ltm/virtual" | python3 -m json.tool | grep fullPath- Distribute traffic across clusters with F5 BIG-IP: terminate TLS at BIG-IP and spread traffic across two clusters, with health monitors and iRules.
- F5 IngressLink documentation: the F5 Container Ingress Services resource that NGINX Gateway Fabric generates.
- F5 Application Services 3 Extension reference: the declaration format F5 Container Ingress Services posts to BIG-IP.
- NGINX Gateway Fabric: the NGINX Gateway Fabric source, including the
ExternalLoadBalancercustom resource definitions. - F5 Container Ingress Services: the F5 Container Ingress Services source and custom resource definitions.
- F5 IPAM Controller: allocates virtual server addresses.
- F5 Container Ingress Services configuration parameters: the full list of deployment options.
- PROXY protocol specification: the header format the iRule generates.