AKS Networking: LoadBalancer and Ingress
Why this matters
Locally, kind/minikube faked LoadBalancer Services (or you used
port-forwarding/NodePort tricks) and any Ingress controller you installed
had no real public IP behind it. On AKS, a LoadBalancer Service actually
provisions an Azure Load Balancer and a public IP address — real,
billable infrastructure — and Ingress gets you a shared entry point
without one IP per Service. Understanding what's real infrastructure
versus what's "just YAML" here matters both for your bill and for
debugging.
Concepts
Service type LoadBalancer on AKS is not simulated. When you
kubectl apply a Service with type: LoadBalancer, the in-cluster cloud
controller manager talks to Azure and provisions a real Azure Load
Balancer (Basic or Standard SKU depending on cluster networking mode)
plus a public IP address resource, and wires it to route traffic to
your Service's backing pods. Both the load balancer and the public IP are
billable Azure resources that persist until the Service (or cluster) is
deleted — this is different from local clusters where type: LoadBalancer
often just meant "pretend, use kubectl port-forward or a NodePort to
actually reach it."
Ingress still works the same at the API level. An Ingress object's
host/path routing rules are unchanged from local Kubernetes. What differs
is what serves as the ingress controller: on AKS you either install
ingress-nginx yourself (same Helm chart you used locally, which
itself provisions one LoadBalancer Service, and therefore one public IP,
shared across every Ingress you define), or use AKS's built-in App
Routing add-on (az aks approuting enable), a managed ingress-nginx
Azure operates for you.
One IP for many Services vs. one IP per Service. This is the core
economic and operational reason to prefer Ingress over multiple
LoadBalancer Services: N Services fronted by one Ingress controller need
only the controller's single public IP/load balancer, not N of them.
DNS. A public IP alone isn't a hostname. For real hostnames you'd
attach an Azure DNS zone or use the load balancer's auto-generated
*.cloudapp.azure.com label — this track uses the raw IP or that
auto-generated label for exercises rather than requiring you to own a
domain.
What AKS manages vs. what you own: Azure's cloud controller manager
handles the actual load balancer/public IP provisioning and keeps them in
sync with your Service objects. You own: which Services are type: LoadBalancer (each one costs its own IP+LB), your Ingress routing rules,
which ingress controller (self-managed vs. App Routing add-on) you run,
and remembering to delete these Services/Ingress before deleting the
cluster's resource group if you want to avoid dangling public IP charges
in edge cases.
Command reference
| Command | What it does | Example |
|---|---|---|
kubectl expose / Service manifest with type: LoadBalancer |
Requests a real Azure Load Balancer + public IP | kubectl expose deployment demo-app --type=LoadBalancer --port=80 --target-port=8080 |
kubectl get service -w |
Watches a Service until EXTERNAL-IP is assigned |
kubectl get service demo-app -w |
az network public-ip list |
Lists public IP resources in a resource group (useful to see the IP behind a Service) | az network public-ip list --resource-group MC_rg-aks-learn_aks-learn_eastus --output table |
az aks approuting enable |
Enables the managed App Routing (ingress-nginx) add-on | az aks approuting enable --resource-group rg-aks-learn --name aks-learn |
az aks approuting disable |
Disables the App Routing add-on | az aks approuting disable --resource-group rg-aks-learn --name aks-learn |
helm install ingress-nginx ingress-nginx/ingress-nginx |
Installs a self-managed ingress-nginx controller (same as local track) | helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace |
kubectl get ingress |
Lists Ingress objects and their address | kubectl get ingress |
kubectl describe ingress |
Shows Ingress rule details and events | kubectl describe ingress <name> |
kubectl get service -n ingress-nginx |
Finds the ingress controller's own LoadBalancer Service and its external IP |
kubectl get service -n ingress-nginx |
curl |
Tests reachability against the external IP/hostname | curl http://<external-ip>/ |
Hands-on exercises
Expose a Deployment directly as
LoadBalancer. Reuse (or recreate) the Deployment from module 02/03. Apply a Service manifest withtype: LoadBalancer. Watch it provision:kubectl get service <name> -w. Verify:EXTERNAL-IPmoves from<pending>to a real IP address within a minute or two (press Ctrl+C once it appears).Confirm the real Azure resource behind it. Run
az network public-ip list --resource-group MC_rg-aks-learn_aks-learn_eastus --output table(adjust theMC_...name to matchaz aks show's output if different). Verify: a public IP resource exists whose address matches the Service'sEXTERNAL-IP.Reach it from outside the cluster. Run
curl http://<external-ip>/from your own machine (not from inside a pod). Verify: you get your application's response back, over the real internet, no port-forwarding involved — the first genuinely-public endpoint in this whole curriculum.Delete that Service and switch to Ingress instead. Run
kubectl delete service <name>(verify the public IP also disappears after a short delay — recheck with the sameaz network public-ip listcommand). Then enable the App Routing add-on:az aks approuting enable --resource-group rg-aks-learn --name aks-learn. Verify:kubectl get pods -n app-routing-system(or similar add-on namespace — checkkubectl get namespacesfor the exact name) shows ingress controller pods running.Create an Ingress routing to your app. Change your app's Service back to
type: ClusterIP(Ingress talks to ClusterIP Services), then write anIngressmanifest with a host/path rule pointing at it, using the ingress class the App Routing add-on registers (check withkubectl get ingressclass). Apply it and verify withkubectl get ingressthat anADDRESSgets populated.Reach your app through the Ingress.
curlthe Ingress's address (with theHostheader if you used a hostname rule:curl -H "Host: demo.example.com" http://<ingress-ip>/). Verify: same response as exercise 3, but now routed through Ingress instead of a dedicated LoadBalancer Service.Add a second app behind the same Ingress to demonstrate the "one IP, many Services" model. Deploy a second small Deployment+ClusterIP Service, add a second path/host rule to the same (or a new) Ingress object, and verify both routes work via
curlagainst the same external IP — confirming you didn't provision a second public IP for the second app.Diagnose and fix: Ingress not getting a public IP. Deliberately create a second Ingress referencing an
ingressClassNamethat doesn't exist (e.g.nginx-typo). Apply it and runkubectl get ingress— verifyADDRESSstays empty. Runkubectl describe ingress <bad-name>and look for events/conditions indicating no controller is claiming it (or check the ingress controller's own logs:kubectl logs -n app-routing-system -l app.kubernetes.io/name=ingress-nginx --tail=50as an alternative angle). Fix it by correctingingressClassNameto matchkubectl get ingressclassoutput, re-apply, and confirmADDRESSpopulates.Clean up. Delete the test Ingress objects and Services you created:
kubectl delete ingress --allandkubectl delete service <any-leftover-LoadBalancer-services>. Confirm no dangling public IPs remain:az network public-ip list --resource-group MC_rg-aks-learn_aks-learn_eastus --output tableshould no longer list ones tied to deleted Services. If you enabled App Routing only for this module and don't need it going forward, disable it:az aks approuting disable --resource-group rg-aks-learn --name aks-learn. If you're done with the cluster for the day, stop or delete it per module 01's cleanup commands.
Independent challenge
No commands given here — figure it out yourself using what you know from this module and earlier ones.
Task: Demonstrate the "one public IP for many apps" economics for yourself, end to end. Get two different small apps running on your cluster (reuse your ACR-hosted image workflow from module 03, conceptually building on that module, for at least one of them), then arrange things so that both are reachable from the public internet through a single shared entry point — not through a dedicated LoadBalancer Service per app. Prove at the Azure level that you provisioned only one public IP for the pair, and reach each app from your own machine over that one address to confirm the routing actually distinguishes them. Then tear the whole thing down and independently confirm no public IP is left dangling in the node resource group — a forgotten public IP or load balancer is exactly the overnight charge this module warns about.
Stuck? One hint
An Ingress object with two host/path rules pointing at two ClusterIP Services shares the single load balancer/IP owned by the ingress controller; compare az network public-ip list on the MC_* resource group before and after to prove only one IP existed.
Common mistakes & troubleshooting
- Creating one
LoadBalancerService per app "because it's easy." Each one is its own public IP + load balancer charge. Prefer one Ingress controller fronting many Services once you have more than one or two apps to expose. - Forgetting to delete a
LoadBalancerService before tearing down a cluster in unusual ways. Normallyaz group deletecleans up everything including public IPs in theMC_*group, but if you ever manage networking resources outside the standard flow, a public IP can be left orphaned and keep billing — always sanity checkaz network public-ip listin theMC_*resource group before assuming cleanup is complete. - Assuming Ingress "just has an IP" instantly. Both the ingress
controller's own Service provisioning and DNS/IP propagation take a
little time; wait and re-check with
kubectl get ingress/kubectl get service -n <ingress-namespace>rather than assuming failure after a few seconds. - Wrong
ingressClassName(or none at all, with multiple controllers installed). If more than one ingress controller exists in a cluster, an Ingress without an explicit class may go unclaimed. Always checkkubectl get ingressclassfor the exact name. - Cost pitfall: forgetting a
LoadBalancerService from an early exercise is still around.kubectl get service --all-namespacesat the end of a session is a cheap habit that catches forgottenLoadBalancerServices (and their IPs) before they bill you overnight.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- What real Azure resources get created when you apply a Service with
type: LoadBalanceron AKS, and how is that different from a local kind/minikube cluster? - Why is Ingress generally cheaper than N
LoadBalancerServices for N apps? - What's the difference between the App Routing add-on and installing ingress-nginx yourself via Helm?
- If
kubectl get ingressshows an emptyADDRESScolumn, what are two things you'd check? - Why does an Ingress route to a
ClusterIPService rather than aLoadBalancerService? - What command would you run to make sure you haven't left any public IPs behind after cleaning up Services?
Show answers
- An Azure Load Balancer and a public IP address resource, both real and
billed. Locally,
type: LoadBalancertypically had no real cloud infrastructure behind it at all (often just simulated or requiring port-forwarding/NodePort workarounds). - Because one ingress controller (and its one
LoadBalancerService/public IP) can route to many backend Services by host/path, so you pay for one IP+LB regardless of how many apps sit behind it, instead of one IP+LB per app. - The App Routing add-on is a version of ingress-nginx that Azure installs, manages, and upgrades for you as part of the cluster; installing ingress-nginx yourself via Helm gives you the same functionality but you own its lifecycle (upgrades, configuration, troubleshooting its pods) directly.
- Whether the
ingressClassNameon the Ingress actually matches an existingIngressClass(kubectl get ingressclass), and whether the ingress controller's own Service has successfully gotten an external IP (kubectl get service -n <ingress-namespace>). - Because the ingress controller itself is the single externally-exposed
entry point (it owns the
LoadBalancerService/public IP); it then routes internally to backend Services, which only need to be reachable inside the cluster, i.e.ClusterIP. az network public-ip list --resource-group <MC_* resource group> --output table, compared againstkubectl get service --all-namespacesto confirm every remaining public IP is backing a Service you still intend to keep.
Next
05-scaling-aks-cluster-autoscaler-and-hpa — let your cluster grow and shrink automatically, both at the pod and node level.