Ingress: routing by domain and path
One entry point on 80/443, with rules that route traffic to Services by domain and path.
One entry point on 80/443, with rules that fan traffic out to Services by domain and path.
Why Ingress exists
A Service can expose an app outside the cluster. NodePort gives you a port on every node, LoadBalancer gives you a public IP. Both work while there's one Service. With ten of them, it falls apart: ten different ports or ten IPs, TLS set up by hand on each, no routing by domain at all.
Ingress solves a different problem. One controller listens on 80/443, reads rules of the form "this host or path goes to this Service", and fans the traffic out. One public address, many apps behind it.
Fan-out by host: one controller at the front, then the request goes to
the right Service based on the Host header.
/-- api.example.com --> Service api
client --> Ingress --+-- web.example.com --> Service web
\-- shop.example.com --> Service shop
Ingress is a declaration, not a controller
The Ingress object itself does nothing. It's a record in etcd: "here are
my rules, someone go act on them." The work is done by the Ingress
Controller: a separate Pod that reads Ingress objects through the API
and configures itself as an HTTP proxy.
An Ingress in a cluster with no controller just sits there, pointless:
the API accepts it, kubectl get ingress shows it, but no traffic goes
anywhere and the ADDRESS column stays empty. And in a cluster with two
controllers (nginx and traefik), you need to say explicitly which one
takes the rule.
That's what the ingressClassName field does. It points to an
IngressClass object, which describes a controller at the cluster level:
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
traefik traefik.io/ingress-controller <none> 42d
The name in ingressClassName is compared to the NAME column
character for character. The API doesn't check that such a class exists:
an Ingress with a typo in the class name is created without an error, it
just never gets picked up by any controller.
If one IngressClass has the annotation
ingressclass.kubernetes.io/is-default-class: "true", an Ingress without
ingressClassName gets picked up by it. Don't rely on this behavior;
set the class explicitly.
The examples below all use ingressClassName: traefik. The manifest
itself doesn't depend on the controller: only the class name changes,
the rules, host and pathType fields stay the same.
ingress-nginx is unmaintained, but it's almost everywhere
For years the most common controller was ingress-nginx, with class
nginx. Its commands, kubectl apply -f .../ingress-nginx/..., are what
sit in half of all repos, articles and old courses.
On March 24, 2026, the kubernetes/ingress-nginx repository was
archived. The last release is controller-v1.15.1, with support claimed
up to cluster version 1.35. There will be no new releases, bugfixes, or
security patches. The reason is simple: one or two maintainers working in
their spare time, and accumulated tech debt that made safe maintenance
unworkable. The InGate project, meant as a successor, was also shelved.
It's easy to overstate the scale of this. This is about the
controller, not the Ingress API. The kind: Ingress object in group
networking.k8s.io/v1 stays GA and isn't going anywhere. Kubernetes
docs call its API frozen: stable, but no new features are coming; they
go into the Gateway API instead. One specific implementation lost
support.
Practice didn't change overnight because of this. According to an estimate in
the Kubernetes Steering Committee's statement, ingress-nginx runs in
roughly half of cloud-native environments, and as of September 2026 it's
still running in production for a huge number of teams. You'll almost
certainly run into it at a new job. You need to be able to read its
annotations (nginx.ingress.kubernetes.io/*).
What this means in practice:
- A working cluster with
ingress-nginxwon't break on its own. It just stopped getting patches, and every new CVE stays open. - It's not worth deploying
ingress-nginxon a new cluster. The official recommendation in the README is direct: pick a Gateway API implementation instead. - Existing rules can be migrated with the
ingress2gatewaytool. No option is a drop-in replacement; you'll have to plan the migration. - Alternatives being deployed now: Traefik, Cilium, Envoy Gateway, NGINX Gateway Fabric, cloud provider controllers.
Check whether it's in a given cluster:
kubectl get pods --all-namespaces \
--selector app.kubernetes.io/name=ingress-nginx
A separate trap in learning clusters: minikube addons enable ingress
still deploys ingress-nginx specifically. kind's docs were rewritten:
ingress there works through cloud-provider-kind (since v0.9.0), no
third-party controller needed.
Minimal manifest
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: helloapp
spec:
ingressClassName: traefik
rules:
- host: helloapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: helloapp
port:
number: 8080
Breakdown:
ingressClassName: traefik. Which controller handles the rule.rules[].host. The controller looks at the incoming request'sHost:header. If it matches this value, the rule fires. Ifhostisn't set, the rule catches any request to this controller.paths[].pathpluspathType. Which URL path matches.backend.service. Where to forward to: the Service's name and its port (the Service'sportfield, not the container'stargetPort). The Service has to live in the same namespace as the Ingress. An Ingress can't reference a Service in a different namespace.
host is compared to the Host header's value as a string, with no
DNS involved. The controller doesn't care where a DNS record points:
curl -H 'Host: helloapp.example.com' http://<controller address>/ hits
this rule, but a request to the same address by IP with no header
doesn't. A wildcard on one label is allowed: *.example.com catches
foo.example.com, but not foo.bar.example.com or example.com.
pathType: three options
- path: /api
pathType: Prefix
Exact. Matches the string exactly, case-sensitive./apicatches/api, but not/api/users.Prefix. Matches the path prefix by segment, separated by/./apicatches/api,/api/,/api/users/1, but not/apiv2: the last segment has to match in full. Case matters here too. This is the most common choice.ImplementationSpecific. Leaves the decision to the controller. nginx and traefik each have their own logic, and it doesn't carry over. Better not to use it.
When several rules match, the longest match wins. /api/users beats
/api, and /api beats /. If the length is the same, Exact beats
Prefix. Order in the YAML doesn't matter.
The imperative way
kubectl create ingress saves time:
kubectl create ingress helloapp \
--class=traefik \
--rule="helloapp.example.com/*=helloapp:8080" \
--dry-run=client -o yaml
The output is ready-made YAML that you just finish and apply. Rule
syntax: host/path=service:port. A trailing asterisk on the path gives
pathType: Prefix, without it you get Exact. The --rule flag can be
repeated to describe several paths in one Ingress.
TLS
A certificate attaches directly to the Ingress:
spec:
tls:
- hosts: [helloapp.example.com]
secretName: helloapp-tls
rules: [...]
secretName points to a Secret of type kubernetes.io/tls with keys
tls.crt and tls.key. The controller pulls the certificate and
terminates TLS itself. The tls block isn't always needed: if a proxy
in front of the cluster already decrypts HTTPS, plain HTTP is what
reaches the Ingress. It's still worth knowing the syntax: it shows up
often in production clusters.
Commands for investigating
kubectl get ingressclass
kubectl get ingress
kubectl describe ingress helloapp
describe shows the class, the rules, and each path's backend. Next to
the backend, kubectl prints the Pod addresses it leads to, or an error:
Rules:
Host Path Backends
---- ---- --------
shop.test
/ webapp:80 (<error: services "webapp" not found>)
If a rule wasn't picked up by a working controller, the reason is
usually in the Events section of the same describe.
What usually breaks
- No Ingress Controller in the cluster: the Ingress sits there with no
effect. In production, check this by looking at the controller's Pods
(
kubectl get pods -A | grep -i ingress). ingressClassNamedoesn't match any IngressClass. The API accepts such an Ingress silently. Check againstkubectl get ingressclass.- The Ingress was created in the wrong namespace. The backend is only
looked up next to the Ingress, and
describewill showservices "..." not found, even though the Service exists in the cluster. - The backend Service doesn't exist, or has no ready addresses. Check
addresses in the EndpointSlice:
kubectl get endpointslice -l kubernetes.io/service-name=<service>. Empty: check the Service's selector and the Pods' labels. The olderkubectl get endpoints <service>still works, but thev1 Endpointsobject is deprecated as of 1.33, and kubectl prints a warning about it. - The backend points at the container's
targetPortinstead of the Service'sport.describewon't always show this as an error; check againstkubectl get svc <service>.
Ingress and the Gateway API
Ingress stays GA and runs in most clusters, but it has a limit: all the configuration (host, path, TLS, fine-grained settings) lives in one object and gets extended through vendor annotations. Its successor is the Gateway API, a newer standard. It splits routing across several objects with different owners and supports TCP, gRPC, and traffic weighting out of the box. For a new project, it's worth looking at. Ingress is taught here because it's everywhere and you need to be able to read it. The difference is covered further along in the track.
Homework
Practice the theory on your own cluster: 1 step with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.
./run.sh start k8s-ingress-basics # set up the starting state
./run.sh verify k8s-ingress-basics # check the current step
In short
- Ingress is only rules in the API. Traffic is handled by the Ingress Controller, a separate Pod with an HTTP proxy inside.
ingressClassNamepicks the controller, and it's better to set it explicitly.pathType: Prefixmatches by path segment, and when several rules match, the longest one wins.- The backend Service has to live in the same namespace as the Ingress.
- TLS attaches through a Secret of type
kubernetes.io/tlsin thespec.tlsblock. - The
ingress-nginxcontroller was archived in March 2026 and no longer gets security patches, but it still runs in roughly half of all clusters. The Ingress API itself is alive and stays GA.
What you'll need for the tasks
kubectl config current-context: make sure you're working in the lab cluster.kubectl get ingressclass,kubectl get ingress -A,kubectl describe ingress <name> -n <namespace>.kubectl get svc -n <namespace>andkubectl get endpointslice -n <namespace> -l kubernetes.io/service-name=<service>.kubectl create ingresswith one or more--rule,kubectl apply -f,kubectl delete ingress.- The
pathType: Prefixand Exact rules from the "pathType: three options" section.
The labs in this lesson have no controller. You write and fix rules, and check them against what the API shows: class, namespace, backend, and its addresses.
Interview-style questions
Interview questions on this topic: 1
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-network-07-ingress
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress