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/subnet2annotations 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.