Users reach the shop through one public address, and behind it sit several pods. Something has to take each connection or request and pick a healthy backend. Azure has two products for it, working at different layers, and each has its own way of failing: a 502 from one, a silently dropped idle connection from the other.
What you need to know already: 9.1 (TCP, RST), 9.21 (HTTP), 9.23 (reverse proxies and load balancers), 9.15 (TLS termination), 16.6 (type: LoadBalancer Services), 16.19 (Ingress), 23.3 (NSGs).
Two products, two layers
Azure Load Balancer works at L4: it forwards TCP/UDP connections by address and port. Application Gateway (App Gateway, AGW) is a managed reverse proxy at L7 (the HTTP layer): it reads each request's host and path. It can also run a WAF (web application firewall: rules that block common attacks like SQL injection, from the OWASP list - a community catalogue of web attacks).
| Azure Load Balancer | Application Gateway | |
|---|---|---|
| layer | L4 (TCP/UDP) | L7 (HTTP/HTTPS) |
| sees | IPs and ports | hosts, paths, headers, cookies |
| TLS | passes it through | terminates it (and can re-encrypt) |
| WAF | no | yes (WAF_v2 SKU, OWASP rules) |
| routing | 5-tuple hash | path/host based, rewrites, redirects |
| in AKS | every type: LoadBalancer Service | Application Gateway Ingress Controller / App Gateway for Containers |
5-tuple hash: the LB picks a backend from source IP, source port, destination IP, destination port and protocol, so one connection always lands on the same backend. AGIC (Application Gateway Ingress Controller) and App Gateway for Containers turn Kubernetes Ingress objects into App Gateway configuration.
Front Door is Azure's global L7 edge in front of either: a CDN (content delivery network - copies of your content served from locations near users), a WAF, and anycast (one IP announced from many locations, so users reach the nearest).
Health probes decide who gets traffic
Both products probe backends (send a test request every few seconds); an unhealthy backend gets nothing. For App Gateway, backend health is the first thing to read on any 502. az network application-gateway show-backend-health -g <rg> -n <gateway> prints each backend server and why it is (un)healthy:
$ az network application-gateway show-backend-health -g rg-oncall-lab -n agw-sysop --query "backendAddressPools[0].backendHttpSettingsCollection[0].servers[]"
[
{
"address": "10.20.0.200",
"health": "Unhealthy",
"healthProbeLog": "Received invalid status code: 404 in the backend server's HTTP response. As per the health probe configuration, 200-399 is the acceptable status code. Either modify probe configuration or resolve backend issues."
}
]
All backends unhealthy = App Gateway answers 502 Bad Gateway to every client, even though the backends may be serving traffic perfectly well to anyone who asks the right path. Probe path, host header, port and expected status codes all have to match what the backend really answers. The incident in this chapter is one of these.
The 4-minute idle timeout
Chapter 9 met this from the TCP side. Here is the Azure side:
- A Standard Load Balancer (the current Standard SKU) tracks each flow (one TCP connection). A flow with no packets for the idle timeout is forgotten. The default is 4 minutes; the configurable range is 4-100 minutes for load-balancing and inbound NAT rules, 4-120 for outbound rules.
- Without TCP reset, the LB silently drops the next packet of a forgotten flow. The client's connection pool thinks the connection is fine; its next request hangs until a TCP timeout. With TCP reset enabled, the LB sends RST to both ends and the client fails fast and reconnects.
AKS (23.20) enables TCP reset on the load balancer it manages (named kubernetes, in the MC_ node resource group), and uses a 30-minute idle timeout on its outbound rule (the rule that carries pods' traffic out to the internet; SNAT, below). Inbound rules for your Services default to 4 minutes, changeable per Service with an annotation:
$ az network lb rule list -g MC_rg-oncall-lab_aks-sysop_westeurope --lb-name kubernetes --query "[].{name:name, port:frontendPort, idle:idleTimeoutInMinutes, reset:enableTcpReset}" -o table
Name Port Idle Reset
---------------------------------------- ---- ---- -----
a3f1c0d2e4b54c6f8a9b0c1d2e3f4a5b-TCP-443 443 4 True
a3f1c0d2e4b54c6f8a9b0c1d2e3f4a5b-TCP-80 80 4 True
$ az network lb outbound-rule list -g MC_rg-oncall-lab_aks-sysop_westeurope --lb-name kubernetes -o table
AllocatedOutboundPorts EnableTcpReset IdleTimeoutInMinutes Name Protocol
---------------------- -------------- -------------------- --------------- --------
0 True 30 aksOutboundRule All
apiVersion: v1
kind: Service
metadata:
name: orders-grpc
annotations:
service.beta.kubernetes.io/azure-load-balancer-tcp-idle-timeout: "30"
spec:
type: LoadBalancer
...
Do not edit the AKS load balancer directly (az network lb rule update): the cloud provider reconciles it from the Service definitions and your change disappears at the next reconcile. Change the Service annotation, or az aks update --load-balancer-idle-timeout for the outbound rule.
The durable fix is on the client: TCP keepalives below the idle timeout (for example every 2-3 minutes), or application-level pings on long-lived connections (gRPC keepalive, database pool validation/idle eviction below 4 minutes). Then the timeout never triggers, whatever the infrastructure does.
SNAT, briefly
SNAT (source network address translation) rewrites the private source address of an outgoing connection to a public one, the way your home router does. Outbound connections from pods and VMs without public IPs leave through the load balancer's frontend (public) IPs, each with ~64,000 SNAT ports shared across the backend pool (the set of VMs behind it). Lots of short outbound connections to the same destination (connection-per-request HTTP clients, no pooling) exhaust them, and new connections fail intermittently. Symptoms look random; the metric is "SNAT connection count / failed" in the LB's metrics. Fixes: connection pooling (reusing connections instead of opening one per request), more outbound IPs, explicit allocated ports, or a NAT Gateway (a dedicated Azure resource for outbound SNAT with many more ports, attached to a subnet).
Reading an App Gateway configuration
$ az network application-gateway show -g rg-oncall-lab -n agw-sysop --query "{sku:sku.name, listeners:httpListeners[].{name:name, host:hostName, proto:protocol}, probes:probes[].{name:name, path:path}, rules:requestRoutingRules[].name}"
{
"listeners": [ { "host": "shop.sysoplab.dev", "name": "https-shop", "proto": "Https" } ],
"probes": [ { "name": "healthz", "path": "/healthz" } ],
"rules": [ "shop" ],
"sku": "WAF_v2"
}
The chain is listener (frontend IP, port, host, certificate) -> rule (which listener goes to which backend) -> backend pool (IPs or FQDNs) + HTTP settings (port, protocol, timeout, host override) + probe. A 502 is almost always in the right half of that chain.
Status codes and who sent them
502 from App Gateway no healthy backend, or the backend reset/closed the connection
504 from App Gateway backend accepted but did not answer within requestTimeout (default 30s)
403 from App Gateway WAF blocked it (check the WAF logs, rule id and matched value)
502/503 from nginx the ingress behind the gateway had no ready endpoints
Look at the response headers and body (curl -v, 9.21): App Gateway's own error pages say "Microsoft-Azure-Application-Gateway/v2" in the Server header. Know which box produced the error before you debug the one behind it.
What you can now do
- Choose between Load Balancer (L4) and App Gateway (L7), and name what each can see.
- Read App Gateway backend health first on any 502.
- Explain the 4-minute idle timeout and SNAT exhaustion, and fix them on the client side.