Home / OCI

OCITroubleshootingAdvanced

OKE LoadBalancer service stuck with EXTERNAL-IP pending: annotations, subnets, policies and limits

Fix an OKE Service of type LoadBalancer that never gets an external IP or has all backends critical - read the cloud controller events, then check shape annotations, LB subnet, IAM policies, service limits, NSGs and health-check rules.

Deepak Bhardwaj4 min read

Applies toOracle Container Engine for Kubernetes (OKE), OCI Load Balancer and Network Load Balancer, kubectl

Symptom

kubectl get svc web -n app
NAME   TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
web    LoadBalancer   10.96.45.12    <pending>     443:31876/TCP  25m

Or the IP is assigned, but the OCI console shows every backend as Critical and clients time out.

Step 1: read the events

The OCI cloud controller manager writes the real reason as events on the service:

kubectl describe svc web -n app | sed -n '/Events/,$p'

Typical messages and what they mean:

Event text (approximate) Cause
Error syncing load balancer ... 404 NotAuthorizedOrNotFound Cluster can't manage LBs or use the subnet/NSG in that compartment
... LimitExceeded / service limit LB or NLB count or bandwidth limit reached
... subnet ... no available IP LB subnet is full
invalid shape / flex errors Wrong shape annotations
... 409 Conflict A previous LB operation is still running; wait, then check again

Step 2: check the annotations

A minimal flexible LB:

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: app
  annotations:
    oci.oraclecloud.com/load-balancer-type: "lb"
    service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "100"
    service.beta.kubernetes.io/oci-load-balancer-internal: "true"
    service.beta.kubernetes.io/oci-load-balancer-subnet1: "ocid1.subnet.oc1..aaaa..."
spec:
  type: LoadBalancer
  selector: { app: web }
  ports: [{ port: 443, targetPort: 8443 }]

For a Network Load Balancer, set oci.oraclecloud.com/load-balancer-type: "nlb". NLB annotations use the oci-network-load-balancer.oraclecloud.com/ prefix, not the LB ones. Mixing prefixes is a common reason annotations are silently ignored.

Annotation names change between OKE versions; check the OKE documentation for your cluster version.

Step 3: check the subnet

oci network subnet get --subnet-id ocid1.subnet.oc1..aaaa... \
  --query "data.{cidr:\"cidr-block\", public:\"prohibit-public-ip-on-vnic\", compartment:\"compartment-id\"}"
  • An internal LB needs a private subnet; a public LB needs a public subnet (prohibit-public-ip-on-vnic = false).
  • The subnet must have free IPs. A /29 or /28 LB subnet fills up fast when every service gets its own LB.
  • A regional subnet is the norm; AD-specific subnets need two subnet1/subnet2 annotations on older setups.

Step 4: check IAM policies

If the LB subnet, NSGs or the LB itself live in a different compartment from the cluster, the cluster needs permission there. Policies for OKE use the cluster as principal, for example:

Allow any-user to manage load-balancers in compartment network-prod where ALL {request.principal.type = 'cluster'}
Allow any-user to use virtual-network-family in compartment network-prod where ALL {request.principal.type = 'cluster'}

Use network-load-balancers for NLBs. Scope conditions to your cluster OCID (request.principal.id) in production rather than every cluster.

Step 5: check limits

# List the limit names for the service, then check availability for the one you need
oci limits definition list --service-name load-balancer --compartment-id ocid1.tenancy.oc1..aaaa... \
  --query "data[].name" --all
oci limits resource-availability get --service-name load-balancer \
  --limit-name <limit-name> --compartment-id ocid1.tenancy.oc1..aaaa...

Limit names differ between LB and NLB and between shapes. If you're at the limit, delete unused LBs (old services in deleted namespaces often leave LBs behind) or request an increase.

Step 6: IP assigned but backends Critical

The LB health-checks the NodePort on each worker node. Traffic goes from the LB subnet to the worker subnet on ports 30000-32767.

  • Worker node security list / NSG must allow ingress from the LB subnet on the NodePort range (or the health-check port).
  • If you set service.beta.kubernetes.io/oci-load-balancer-security-list-management-mode: "None", OKE won't add those rules for you; you must manage them.
  • With externalTrafficPolicy: Local, only nodes running a pod pass health checks. Some backends being Critical is expected; all of them is not.
  • For NLB with source IP preservation, the return path must be symmetric; see NLB source IP preservation.
kubectl get svc web -n app -o jsonpath='{.spec.ports[*].nodePort}{"\n"}'
# from a host in the LB subnet:
nc -vz <worker-node-ip> <nodePort>

Prevent it next time

  • Standardise one Service template per pattern (public LB, internal LB, NLB) with tested annotations.
  • Size LB subnets for growth (/24) and prefer an ingress controller behind a single LB instead of one LB per service.
  • Clean up services before deleting namespaces so LBs are released.
  • Related: OCI LB all backends critical.