# Use F5 BIG-IP as an external load balancer
Type of document: How-to guide
Product: FABRIC
> Configure an ExternalLoadBalancer so F5 BIG-IP acts as the external load balancer for an NGINX Gateway Fabric Gateway in a single cluster, preserving client IP addresses with the PROXY protocol.
---
This guide describes how to use an F5 BIG-IP system as the external load balancer for an NGINX Gateway Fabric Gateway.
## Overview
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.
### How configuration reaches BIG-IP
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.
```mermaid
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]
```
## Before you begin
You need:
- A Kubernetes cluster.
- An F5 BIG-IP system running version 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:
```shell
export BIGIP_ADDRESS="192.0.2.10:443"
export BIGIP_USERNAME="admin"
export BIGIP_PASSWORD=""
export IPAM_ADDRESS_RANGE="192.0.2.100-192.0.2.110"
```
- `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.
- `IPAM_ADDRESS_RANGE` is 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](#install-the-f5-ipam-controller).
Two more variables are set later, once their values exist:
- `ALLOCATED_ADDRESS` is the virtual server address the F5 IPAM Controller allocates, read from the `IngressLink` status in [Verify the configuration](#verify-the-configuration).
- `NGINX_POD_NAME` is the name of an NGINX Pod, used when reading its logs.
## Prepare BIG-IP
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.
### 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.
### TCP iRule
This guide uses a TCP iRule named `Proxy_Protocol_iRule`:
```text
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:
```shell
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:
```json
{
"name": "Proxy_Protocol_iRule",
"fullPath": "/Common/Proxy_Protocol_iRule",
"apiAnonymous": "when SERVER_CONNECTED { ... }"
}
```
## Install F5 Container Ingress Services
### Install the F5 IPAM Controller
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:
```yaml
kubectl apply -f - <
```
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:
```shell
export ALLOCATED_ADDRESS=$(kubectl get ingresslink gateway-nginx -o jsonpath='{.status.vsAddress}')
```
Send a request through BIG-IP:
```shell
curl -H "Host: cafe.example.com" http://$ALLOCATED_ADDRESS/coffee
```
The request returns `200 OK` with a response body from the backend application.
```text
Server address: 10.42.0.43:8080
Server name: coffee-7b9578cff9-t7r7v
Date: 05/Aug/2026:14:33:40 +0000
URI: /coffee
Request ID: a2ae0944885fdf99bb5f86038aeae84f
```
Confirm NGINX sees the original client address:
```shell
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 coffee
```
The access log records the address of the machine you sent the request from.
## Troubleshooting
### No IngressLink is created
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:
```shell
kubectl get deploy -n nginx-gateway ngf-nginx-gateway-fabric \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="nginx-gateway")].args}'
```
### The IngressLink has no status
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:
```shell
kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr
```
### No address is allocated
Confirm F5 Container Ingress Services was deployed with `args.ipam=true`, then check the F5 IPAM Controller logs:
```shell
kubectl logs -n kube-system -l app=f5-ipam-controller --tail=20
```
A label that does not match a configured pool is reported directly:
```text
[PROV] IPAM LABEL: gatewaylink Not Found
```
Set `ipamLabel` on the `ExternalLoadBalancer` to a pool name from the `args.ip_range` map used when installing the F5 IPAM Controller.
### The AS3 declaration is rejected
Read the BIG-IP response in the F5 Container Ingress Services logs, which usually names the problem:
```shell
kubectl logs -n kube-system deploy/f5-cis-f5-bigip-ctlr | grep -E "AS3\]\[POST\]|response:"
```
### F5 Container Ingress Services reports that AS3 is not installed
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](https://clouddocs.f5.com/products/extensions/f5-appsvcs-extension/latest/userguide/troubleshooting.html) in the F5 documentation.
After restoring AS3, delete the Pod so it retries without waiting out its backoff:
```shell
kubectl delete pod -n kube-system -l app=f5-cis-f5-bigip-ctlr
```
### A pool is empty
Confirm 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:
```shell
kubectl get svc gateway-nginx -o jsonpath='{.spec.type}{"\n"}{.spec.ports}'
kubectl describe gateways.gateway.networking.k8s.io gateway
```
### NGINX logs show an internal address as the client
The 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:
```shell
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:
```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.
## 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
- [Distribute traffic across clusters with F5 BIG-IP](/ngf/external-loadbalancers/gateway-link/multicluster.md): terminate TLS at BIG-IP and spread traffic across two clusters, with health monitors and iRules.
- [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.
- [NGINX Gateway Fabric](https://github.com/nginx/nginx-gateway-fabric): the NGINX Gateway Fabric source, including the `ExternalLoadBalancer` custom resource definitions.
- [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.
- [F5 Container Ingress Services configuration parameters](https://clouddocs.f5.com/containers/latest/userguide/config-parameters.html): the full list of deployment options.
- [PROXY protocol specification](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt): the header format the iRule generates.