65.1 Ingress and Ingress controllers: one entry point for many Services
Why: the problem
In Chapter 62 every app got its own Service. When Swiggy runs a menu service, a cart service, an orders service and a partner portal, giving each one a LoadBalancer Service means four cloud load balancers, four public IP addresses, four bills and four places to install a certificate. Users do not want four addresses either. They type one name, such as swiggy.com, and expect the right page to come back whether the URL ends in /menu or /cart. Ingress solves this. An Ingress object is a list of HTTP rules: when the host is shop.example.com and the path starts with /api, send the request to the api Service on port 80; everything else goes to the web Service. The Ingress object itself does nothing. A separate program called an Ingress controller watches all Ingress objects in the cluster and configures a real reverse proxy, such as NGINX, Traefik, HAProxy, Envoy or a cloud load balancer, to follow those rules. Only the controller needs a public address; it then fans traffic out to many ClusterIP Services inside the cluster. Which controller you install matters. The community ingress-nginx project was the most common choice for years, but Kubernetes SIG Network and the Security Response Committee announced its retirement in November 2025 and retired it on 24 March 2026. Existing installs still work, and its manifests stay available, so it is fine for learning on kind, but it no longer receives security fixes. New production clusters choose a maintained controller (Traefik, HAProxy Ingress, the F5 NGINX Ingress Controller, the AWS Load Balancer Controller, Contour) or move to Gateway API, which we cover in 65.5. The good news: the Ingress object you write is the same for all of them; only ingressClassName changes. In this lesson we create a kind cluster that maps ports 80 and 443 to your laptop, install a controller, expose two apps behind one Ingress, read how the controller turned rules into proxy config, and see the default backend and IngressClass in action.
What: every term in plain English
- Ingress — A Kubernetes object holding HTTP and HTTPS routing rules: host, path and the backend Service.
- Ingress controller — The program that reads Ingress objects and configures a real proxy to follow them.
- IngressClass — A cluster object naming a controller; ingressClassName in an Ingress picks which controller handles it.
- Backend — The Service name and port that receives matching requests.
- Default backend — Where requests go when no rule matches; usually a 404 page.
- pathType — Prefix matches by path segment, Exact matches the whole path, ImplementationSpecific is controller-specific.
- Reverse proxy — A server that receives client requests and forwards them to backend servers.
- extraPortMappings — kind setting that forwards ports on your laptop to a node container.
- Annotation — Controller-specific key-value setting on an Ingress, such as a rewrite or timeout.
- Layer 7 — HTTP level routing using host, path and headers, unlike layer 4 which sees only IP and port.
How: step by step
- Create a kind cluster with ports 80 and 443 mapped. curl localhost reaches the node.
- Install an Ingress controller from a pinned manifest. Controller pod Running.
- Check the IngressClass. nginx class exists.
- Deploy two small apps with ClusterIP Services. web and api ready.
- Write one Ingress with two paths. Rules accepted.
- Test with curl and a Host header. Each path reaches its app.
- Read the generated proxy config. Rules became NGINX locations.
- Clean up or keep for 65.2. Cluster stays.
Example 1: Create a kind cluster ready for Ingress and install a controller
Goal. Create a kind cluster ready for Ingress and install a controller.
kind-ch65.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ch65
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
- role: worker
install-controller.sh
kind create cluster --config kind-ch65.yaml
# ingress-nginx-kind-v1.11.3.yaml: the kind provider manifest saved from the official docs, pinned and reviewed.
# The project is retired (24 March 2026); we use it only as a learning lab on kind.
kubectl apply -f ingress-nginx-kind-v1.11.3.yaml
kubectl wait -n ingress-nginx --for=condition=ready pod -l app.kubernetes.io/component=controller --timeout=180s
kubectl get ingressclass
What each line does
node-labels ingress-ready=true— The kind manifest uses a nodeSelector on this label, so the controller lands on the node with port mappings.extraPortMappings— Port 80 and 443 on your laptop go into the control plane container, where the controller listens with hostPort.Pinned manifest— A saved, reviewed file with a fixed version, so the lab gives the same result next month.kubectl wait— Blocks until the controller pod is Ready instead of guessing with sleep.get ingressclass— Shows the class name you will put in ingressClassName.
Green output
$ bash install-controller.sh
Creating cluster "ch65" ...
✓ Ensuring node image (kindest/node:v1.34.0)
✓ Preparing nodes
Set kubectl context to "kind-ch65"
namespace/ingress-nginx created
deployment.apps/ingress-nginx-controller created
ingressclass.networking.k8s.io/nginx created
pod/ingress-nginx-controller-7c9d8b6f5-x4k2p condition met
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx none 40s
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
404
Expected result. A 404 from localhost is good news: the request reached the controller, and its default backend answered because no Ingress exists yet.
Example 2: Expose two apps behind one Ingress by path
Goal. Expose two apps behind one Ingress by path.
apps.sh
kubectl create ns shop
kubectl create deployment web -n shop --image=hashicorp/http-echo:1.0 -- /http-echo -listen=:8080 -text="web home"
kubectl create deployment api -n shop --image=hashicorp/http-echo:1.0 -- /http-echo -listen=:8080 -text="api v1"
kubectl expose deployment web -n shop --port=80 --target-port=8080
kubectl expose deployment api -n shop --port=80 --target-port=8080
kubectl rollout status deploy/web -n shop && kubectl rollout status deploy/api -n shop
shop-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
spec:
ingressClassName: nginx
rules:
- host: shop.local
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
What each line does
ClusterIP Services— web and api are not public; only the controller is.ingressClassName: nginx— Tells which controller owns this Ingress; without it a default class may or may not pick it up.host: shop.local— Rules apply only when the HTTP Host header is shop.local./api before /— The controller sorts by the longest match, so /api/orders goes to api and everything else to web.pathType Prefix— /api matches /api and /api/orders but not /apix, because Prefix matches whole segments.
Green output
$ bash apps.sh
deployment "web" successfully rolled out
deployment "api" successfully rolled out
$ kubectl apply -f shop-ingress.yaml
ingress.networking.k8s.io/shop created
$ kubectl get ingress -n shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop nginx shop.local localhost 80 20s
$ curl -s -H "Host: shop.local" http://localhost/
web home
$ curl -s -H "Host: shop.local" http://localhost/api/orders
api v1
$ curl -s -H "Host: shop.local" http://localhost/apix
web home
$ curl -s -o /dev/null -w "%{http_code}\n" -H "Host: other.local" http://localhost/
404
Expected result. One entry point on port 80 now serves two Services. The Host header picks the rule set, the path picks the Service, and a host with no rule gets the default 404.
Example 3: See what the controller built and use IngressClass well
Goal. See what the controller built and use IngressClass well.
inspect.sh
POD=$(kubectl get pod -n ingress-nginx -l app.kubernetes.io/component=controller -o name)
kubectl exec -n ingress-nginx $POD -- cat /etc/nginx/nginx.conf | grep -n "server_name shop.local" -A3
kubectl describe ingress shop -n shop
kubectl get endpointslices -n shop
kubectl annotate ingressclass nginx ingressclass.kubernetes.io/is-default-class=true
no-class.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: docs
namespace: shop
spec:
rules:
- host: docs.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
What each line does
nginx.conf— The controller wrote a server block per host and a location per path.describe ingress— Shows each rule and the pod IPs behind each backend.EndpointSlices— The controller sends traffic straight to pod IPs from these, skipping kube-proxy.is-default-class— Ingresses without ingressClassName now go to this class.Only one default— Two default classes make new Ingresses ambiguous; keep exactly one.
Green output
$ bash inspect.sh
212: server {
213: server_name shop.local ;
214: listen 80 ;
Rules:
Host Path Backends
---- ---- --------
shop.local
/api api:80 (10.244.1.5:8080)
/ web:80 (10.244.2.4:8080)
NAME ADDRESSTYPE PORTS ENDPOINTS
api-8xk2q IPv4 8080 10.244.1.5
web-p9v7d IPv4 8080 10.244.2.4
ingressclass.networking.k8s.io/nginx annotated
$ kubectl apply -f no-class.yaml
$ curl -s -H "Host: docs.local" http://localhost/
web home
Expected result. The rules became plain NGINX config, traffic goes directly to pod IPs, and marking one IngressClass as default lets teams skip ingressClassName safely.
Every command, step by step (kind lab with kubectl)
-
Cluster
kind create cluster --config kind-ch65.yamlWhat it does: Ports 80 and 443 mapped.
Set kubectl context to "kind-ch65" -
Controller
kubectl apply -f ingress-nginx-kind-v1.11.3.yamlWhat it does: Installed.
deployment.apps/ingress-nginx-controller created -
Wait
kubectl wait -n ingress-nginx --for=condition=ready pod -l app.kubernetes.io/component=controller --timeout=180sWhat it does: Ready.
condition met -
Class
kubectl get ingressclassWhat it does: nginx.
nginx k8s.io/ingress-nginx -
Default 404
curl -s -o /dev/null -w "%{http_code}" http://localhost/What it does: No rules yet.
404 -
Apps
bash apps.shWhat it does: Two Services.
deployment "api" successfully rolled out -
Ingress
kubectl apply -f shop-ingress.yamlWhat it does: Rules.
ingress.networking.k8s.io/shop created -
Web
curl -s -H "Host: shop.local" http://localhost/What it does: Root path.
web home -
API
curl -s -H "Host: shop.local" http://localhost/api/ordersWhat it does: Prefix match.
api v1 -
Describe
kubectl describe ingress shop -n shopWhat it does: Backends and pod IPs.
/api api:80 (10.244.1.5:8080) -
Default class
kubectl annotate ingressclass nginx ingressclass.kubernetes.io/is-default-class=trueWhat it does: One default.
ingressclass.networking.k8s.io/nginx annotated
When it is red 1
curl: (7) Failed to connect to localhost port 80
Cause. The cluster was created without extraPortMappings, or another program uses port 80.
Fix, step by step
- Recreate with kind-ch65.yaml.
- Stop the local web server using port 80.
When it is red 2
404 Not Found from nginx for a host you defined
Cause. Host header does not match, or ingressClassName points to a class no controller owns.
Fix, step by step
- curl -H "Host: shop.local".
- kubectl get ingressclass and compare.
When it is red 3
503 Service Temporarily Unavailable
Cause. Backend Service has no ready endpoints.
Fix, step by step
- kubectl get endpointslices -n shop.
- Fix the selector or readiness probe.
When it is red 4
ADDRESS column stays empty
Cause. Controller has not processed the Ingress, often a wrong class.
Fix, step by step
- kubectl logs of the controller pod.
- Set ingressClassName.
When it is red 5
Controller pod Pending
Cause. No node has ingress-ready=true.
Fix, step by step
- Use the kind config with node-labels.
- kubectl get nodes --show-labels.
Practice tasks
Easy Task 1: Cluster
Why: Create.
What to do: Create kind with ports 80 and 443 mapped.
- kind-ch65.yaml.
Solution
docker ps --filter name=ch65Easy Task 2: Controller
Why: Install.
What to do: Apply the pinned controller manifest.
- Wait for Ready.
Solution
kubectl get pods -n ingress-nginxEasy Task 3: Apps
Why: Deploy.
What to do: Create web and api with ClusterIP Services.
- http-echo.
Solution
kubectl get svc -n shopMedium Task 4: Paths
Why: Route.
What to do: Apply shop-ingress.yaml.
- /api and /.
Solution
curl -s -H "Host: shop.local" http://localhost/apiMedium Task 5: Host
Why: Test.
What to do: Send a wrong Host header.
- Expect 404.
Solution
curl -s -o /dev/null -w "%{http_code}" -H "Host: x.local" http://localhost/Medium Task 6: Config
Why: Inspect.
What to do: Find the server block in nginx.conf.
- kubectl exec.
Solution
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- nginx -THard Task 7: Default
Why: Class.
What to do: Make nginx the default class and apply no-class.yaml.
- One default only.
Solution
kubectl get ingressclass -o yamlHard Task 8: Swiggy
Why: Main task.
What to do: Three apps behind partner.local.
- Three paths.
Solution
kubectl get ingress -AHomework: try at home
For Swiggy's partner portal, run three apps (portal, menu, payouts) behind one Ingress on partner.local with paths /, /menu and /payouts, prove each path with curl and a Host header, show the 404 for an unknown host, and mark one IngressClass as default.
Recap
- Ingress holds HTTP rules; a controller makes them real.
- One controller address can serve many hosts and paths.
- ingressClassName picks the controller; mark exactly one class as default.
- pathType Prefix matches whole segments and longest match wins.
- ingress-nginx was retired on 24 March 2026; use it only for learning and pick a maintained controller or Gateway API for production.
FAQ
Q. Ingress or LoadBalancer Service?
Use a LoadBalancer Service for non-HTTP traffic such as databases or MQTT. Use Ingress or Gateway API for HTTP and HTTPS so many apps share one load balancer and one certificate setup.
Q. Is NGINX Ingress from F5 the same as ingress-nginx?
No. They are two different projects that both use NGINX. The F5 controller is still maintained; the community ingress-nginx is retired.
Q. Can two controllers run in one cluster?
Yes. Give each its own IngressClass, for example internal and public, and set ingressClassName per Ingress.
Q. Does Ingress handle TCP or UDP?
Not in the standard API. Some controllers have their own extras, but Gateway API has TCPRoute and UDPRoute for that.
Interview questions
-
What is the difference between Ingress and an Ingress controller?
Model answer: Ingress is a rules object; the controller is the running proxy that reads those rules and serves traffic.
-
Why not one LoadBalancer per Service?
Model answer: Cost, more public IPs, more certificates and more places to secure. Ingress shares one entry point.
-
How does the controller reach pods?
Model answer: It reads EndpointSlices and proxies straight to pod IPs.
-
What changed for ingress-nginx in 2026?
Model answer: It was retired on 24 March 2026; no new releases or security patches, so plan migration.
Quick quiz
Q1. What does an Ingress object do on its own?
Nothing; a controller must act on it.
Q2. Which field picks the controller?
ingressClassName.
Q3. Which match type respects path segments?
Prefix.
Q4. What answers when no rule matches?
The default backend, usually 404.
Q5. Why was ingress-nginx retired?
Not enough maintainers; no more security fixes after 24 March 2026.
Samajla ka?