Ingress and Getting Traffic In
One entry point, many services. Ingress rules, hostnames and paths, TLS termination, and why an Ingress does nothing without a controller behind it.
One entry point, many services. Ingress rules, hostnames and paths, TLS termination, and why an Ingress does nothing without a controller behind it.
Your application is reachable inside the cluster at web. Nobody
outside can use that. The options you have so far are a high-numbered
port on every node, or one cloud load balancer per Service — an
address and a monthly bill each, with no way to say "this hostname
goes here and that path goes there".
Ingress is the object that fixes it: one entry point, routing HTTP by hostname and path to as many Services as you like, terminating TLS on the way through. By the end of this lesson you will have one working locally, and — just as importantly — you will know why an Ingress on its own does absolutely nothing.
This is the distinction that causes the most wasted time in this topic, so it comes first.
A set of routing rules. Inert.
A YAML object saying "this hostname and path go to that Service". Creating it changes nothing about traffic — it is a request, written down.
A program that reads them and acts.
Runs in your cluster, watches Ingress objects, and configures a real proxy to match. This is what actually receives connections.
Unlike Deployments and Services, Kubernetes ships no controller for this. You install one.
No controller, no effect. An Ingress applied to a cluster without one
sits there indefinitely, valid and inert, with an empty ADDRESS
column — which reads exactly like a bug and is not one.
For kind, the controller needs somewhere to receive traffic, so the cluster has to be created with a node that maps host ports:
On minikube it is one line, which is the argument for minikube if you want the shortest path:
kubectl wait is worth noticing on its own — it blocks until a
condition is true, which is far better than sleeping and hoping in a
script.
With a Deployment and a ClusterIP Service called web already
running:
An ADDRESS appearing is the controller reporting that it has accepted
the rule. While that column is empty, nothing is routing.
The curl -H "Host: ..." form is how you test a hostname you have not
put in DNS. Routing happens on the Host header, so setting it
directly is equivalent to having a real DNS record, without editing
anything on your machine.
Four fields carry the meaning:
ingressClassName says which controller should act on this
object. A cluster can run several; a mismatch here is the second cause
of a permanently empty ADDRESS. List them with
kubectl get ingressclass.
host is the hostname to match. Omit it and the rule matches any
hostname, which is fine for a single-application cluster.
pathType is required, and there are three:
Prefix matches path segments — /api matches /api/users but not
/apifoo. Exact matches only that exact path.
ImplementationSpecific hands interpretation to the controller, which
is how regular expressions become available.
backend names a Service and a port — not a pod. Ingress routes
to Services, and the Service does the rest.
This is what Ingress is for. One entry point, many destinations.
By hostname, which is the cleanest arrangement:
Or by path, under one hostname:
Order the paths from most specific to least, and keep / last. Most
controllers match longest-prefix regardless, but relying on that
across implementations is a bet you do not need to take.
Terminating HTTPS at the Ingress means one place holds certificates and your applications speak plain HTTP internally.
The certificate lives in a Secret of type kubernetes.io/tls:
Note the backend port stays 80. The Ingress decrypts, then forwards HTTP inside the cluster.
In practice nobody manages those files by hand. cert-manager is the standard answer: install it, point it at Let's Encrypt, annotate the Ingress, and certificates are issued and renewed automatically.
The Secret named in tls is then created and refreshed for you.
Bad — routes to a Service that does not exist under that name:
Good — the Service's name and the Service's port:
Both apply without complaint, because the Ingress is only a rule and
nothing checks that its target resolves. The result is a 503 from
the controller with no clue in your application's logs — because the
request never reached your application. Two names and two ports are in
play, and the Ingress wants the Service's.
The check is one command:
describe ingress shows the backend it found, or reports the Service
as unresolvable — which settles it immediately.
Work outside in. Each step rules out one layer.
Mapping the common answers:
| Symptom | Usual cause |
|---|---|
ADDRESS stays empty | No controller, or wrong ingressClassName |
404 from the controller | Host header or path does not match a rule |
503 from the controller | Backend Service missing or has no endpoints |
| Connection refused | Nothing listening on the controller's ports |
| TLS warning | Wrong or missing Secret, or host mismatch |
The distinction between the two error codes is the useful one, and it tells you which half of the setup to look at.
No rule matched the request.
The host header or the path did not match anything you wrote.
Check the Host: your client actually sent, and the
pathType.
The problem is in the Ingress object.
A rule matched, and there was nothing behind it.
The Service named in the backend is missing, or it exists and has no endpoints.
The Ingress is fine. Go back to
kubectl describe service | grep Endpoints.
Traffic now reaches your application from outside on a real hostname. What the application still lacks is anything to configure it — the database URL, the log level, the credentials — which is the next lesson, ConfigMaps and Secrets.
Before that, get the local Ingress working end to end with two
Services and two hostnames. Watching one entry point split traffic
by Host header is the moment the object stops feeling like
overhead.
NAME CLASS HOSTS ADDRESS PORTS AGE
web nginx web.localhost localhost 80 20s# kind-ingress.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443kind delete cluster --name learning
kind create cluster --name learning --config kind-ingress.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/\
ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90sminikube addons enable ingressapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
spec:
ingressClassName: nginx
rules:
- host: web.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80kubectl apply -f ingress.yaml
kubectl get ingresscurl -H "Host: web.localhost" http://localhost/spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80kubectl create secret tls web-tls \
--cert=tls.crt --key=tls.keyspec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: web-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prodbackend:
service:
name: web-deployment # the Deployment's name
port:
number: 8000 # the container's portbackend:
service:
name: web # the Service
port:
number: 80 # the Service's portkubectl get service web # does this name exist
kubectl describe ingress web # what the controller resolved# 1. Is a controller running at all?
kubectl get pods -n ingress-nginx
kubectl get ingressclass
# 2. Did the controller accept the rule? (ADDRESS not empty)
kubectl get ingress
kubectl describe ingress web
# 3. Does the Service behind it have endpoints?
kubectl describe service web | grep Endpoints
# 4. What does the controller say it is doing?
kubectl logs -n ingress-nginx -l \
app.kubernetes.io/component=controller --tail=50apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx # which controller acts on this
tls:
- hosts: [app.example.com]
secretName: web-tls # a kubernetes.io/tls Secret
rules:
- host: app.example.com # matched on the Host header
http:
paths:
- path: /api
pathType: Prefix # Prefix | Exact
backend:
service:
name: api # the SERVICE name
port:
number: 80 # the SERVICE port
- path: / # keep the catch-all last
pathType: Prefix
backend:
service:
name: web
port:
number: 80# An Ingress needs a controller — nothing is built in
minikube addons enable ingress
kubectl get pods -n ingress-nginx
kubectl get ingressclass
# Everyday
kubectl apply -f ingress.yaml
kubectl get ingress # ADDRESS empty = not routing
kubectl describe ingress web # rules and resolved backends
# Testing a hostname that is not in DNS
curl -H "Host: app.example.com" http://localhost/
# Diagnosing, outside in
kubectl get pods -n ingress-nginx # 1. controller alive
kubectl get ingress # 2. rule accepted
kubectl describe service web | grep Endpoints # 3. pods behind it
kubectl logs -n ingress-nginx -l \
app.kubernetes.io/component=controller --tail=50 # 4. its view
# Reading the status code
# 404 from the controller -> no rule matched host/path
# 503 from the controller -> rule matched, backend empty or missing
# empty ADDRESS -> no controller, or wrong ingressClassName
# TLS
kubectl create secret tls web-tls --cert=tls.crt --key=tls.key
# in production, let cert-manager create and renew it