Ingress
HTTP routing and TLS in front of Services.
Your app runs fine when you port-forward straight to it, a direct tunnel from your laptop to the running container. Point a browser at the real hostname and you get a timeout, a blank page, or somebody else's app. Nine times out of ten the missing piece is Ingress. The tenth time it's an Ingress that exists but has nothing running to carry out its rules, and that's the nastier case, because nothing prints an error to tell you.
Here's the problem Ingress solves. Every Service you expose with type LoadBalancer asks the cloud for its own public load balancer, its own IP address, and its own line on the bill. Ten web apps means ten load balancers and no shared place to handle certificates or routing. Now think of a large office building. You don't give every team its own street entrance and its own receptionist. There's one front door and one reception desk, and the receptionist reads who each visitor asked for and sends them to the right floor. Ingress is that reception desk for web traffic: one public entry point that routes each HTTP request to the right internal Service (Kubernetes' stable handle for an app) by hostname and URL path.
The object is inert, the controller does the work
An Ingress object is just a list of routing rules, written down and stored. By itself it moves no traffic at all. You also need an Ingress controller, a real program (ingress-nginx, Traefik, HAProxy, or a cloud provider's own) running as Pods inside the cluster. A Pod is the smallest thing Kubernetes runs, one or more containers sharing an address. The controller watches the API server (the cluster's single front door and the only component that writes cluster state) for Ingress objects, reads their rules, and rewrites its own proxy configuration to match. It watches rather than polls, so a change lands almost instantly. The real traffic path is one cloud load balancer, into the controller's Pods, which forward to the right ClusterIP Service (an address that only exists inside the cluster), which lands on an application Pod.
This split causes the single most common Ingress mistake. Someone applies an Ingress on a fresh cluster that has no controller installed, then waits for the site to come up. It never does, and nothing complains. The rules just sit in etcd, the cluster's key-value database, being ignored. Before you debug anything downstream, confirm a controller is actually running and that an IngressClass exists for your rules to attach to.
$ kubectl get pods -n ingress-nginxNAME READY STATUS RESTARTS AGEingress-nginx-controller-7c9f8d5b4f-2xk9p 1/1 Running 0 6d$ kubectl get ingressclassNAME CONTROLLER PARAMETERS AGEnginx k8s.io/ingress-nginx <none> 6d
Rules: hosts, paths, and the pathType trap
Two things route a request: the host it asked for and the path it wants. ingressClassName ties this object to one controller, since a cluster can run several. The field people quietly get wrong is pathType. Prefix matches a whole subtree, so /api matches /api and /api/orders alike. Exact matches one string and nothing beneath it. A third value, ImplementationSpecific, hands the decision to the controller. Mix these up and you get the maddening case of a route that almost works. Anything past plain routing, like a rate limit or a URL rewrite, you reach through annotations, which are key-value hints the controller reads off the object.
The same object routes two ways at once. By host, shop.acme.io and blog.acme.io can send visitors to different Services from a single Ingress. By path, everything under one hostname splits by URL, which is what the file below does: send /api to the api Service and everything else to web. Get the hostname wrong by one character and the request falls through to whatever default the controller keeps, often a bare 404. Big sites use both styles together. Pick the app by its hostname first, then the right piece of that app by its path.
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: shopannotations:nginx.ingress.kubernetes.io/ssl-redirect: "true"spec:ingressClassName: nginxtls:- hosts: [shop.acme.io]secretName: shop-tlsrules:- host: shop.acme.iohttp:paths:- path: /apipathType: Prefixbackend:service:name: apiport:number: 80- path: /pathType: Prefixbackend:service:name: webport:number: 80
$ kubectl apply -f ingress.yamlingress.networking.k8s.io/shop created$ kubectl get ingress shopNAME CLASS HOSTS ADDRESS PORTS AGEshop nginx shop.acme.io 203.0.113.10 80, 443 25s
The ADDRESS column is your first signal. Once it shows an external IP or hostname, the controller has accepted the object and pointed the load balancer at it. A blank ADDRESS a minute later means no controller ever claimed the Ingress, and that is nearly always a wrong or missing ingressClassName, so nothing is listening for that class.
An ADDRESS on the object is not the finish line. That IP or hostname is only where the load balancer sits. A browser still has no idea that shop.acme.io belongs there until you add a DNS record, the internet's address book, pointing the hostname at that address. Skip it and you get the classic afternoon where the site answers by raw IP but never by name, with an Ingress that looks flawless the whole time.
TLS termination in one place
TLS, short for Transport Layer Security, is the lock behind the HTTPS padlock. The receptionist analogy still holds: instead of every office printing its own visitor badges, reception issues and checks them in one spot. The controller terminates TLS, which means it holds the certificate and private key, decrypts incoming HTTPS at the edge, and then speaks plain HTTP to your Pods inside the cluster. The certificate lives in a Secret of type kubernetes.io/tls. In practice cert-manager issues and renews these automatically from a certificate authority like Let's Encrypt, so nobody is copying key files around by hand.
$ kubectl create secret tls shop-tls --cert=tls.crt --key=tls.keysecret/shop-tls created$ kubectl get secret shop-tlsNAME TYPE DATA AGEshop-tls kubernetes.io/tls 2 10s$ curl -sI https://shop.acme.io/ | head -1HTTP/2 200
When it breaks, verify in the order traffic flows
Debug an Ingress the same way a request travels through it. Is there an ADDRESS? Then does the controller's own events log show it synced the rule? Then does the backend Service actually have endpoints, meaning ready Pods behind it? A 503 coming from the controller almost always means the Service has no ready endpoints to send to. A 404 from the controller means no rule matched the host or path you sent. describe surfaces the controller's events, and EndpointSlices tell you whether the Service has anywhere to route traffic at all. The controller's own logs are the other half of the story, since ingress-nginx prints each config reload and any host it refused to load.
$ kubectl describe ingress shop | sed -n '/Events/,$p'Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Sync 30s nginx-ingress-controller Scheduled for sync$ kubectl get endpointslices -l kubernetes.io/service-name=apiNAME ADDRESSTYPE PORTS ENDPOINTS AGEapi-abc12 IPv4 80 10.244.1.7 6d
No controller means Ingress objects do nothing. Install and watch the controller pods first.
Path types Prefix versus Exact change matching. Trailing slashes have ruined more cutovers than TLS.
TLS secrets must live in the right namespace for most controllers. Cross-namespace tricks are controller-specific.
Try this
Confirm an Ingress controller is running, apply a basic Ingress host rule to a Service, and curl with a Host header. Add TLS only if you have a test secret ready.
$ kubectl get pods -n ingress-nginxNAME READY STATUS RESTARTS AGEingress-nginx-controller-7c9f8d5b4f-2xk9p 1/1 Running 0 6d$ kubectl get ingressclassNAME CONTROLLER PARAMETERS AGEnginx k8s.io/ingress-nginx <none> 6d$ kubectl apply -f ingress.yamlingress.networking.k8s.io/shop created$ kubectl get ingress shopNAME CLASS HOSTS ADDRESS PORTS AGEshop nginx shop.acme.io 203.0.113.10 80, 443 25s$ kubectl create secret tls shop-tls --cert=tls.crt --key=tls.keysecret/shop-tls created$ kubectl get secret shop-tlsNAME TYPE DATA AGEshop-tls kubernetes.io/tls 2 10s$ curl -sI https://shop.acme.io/ | head -1HTTP/2 200$ kubectl describe ingress shop | sed -n '/Events/,$p'Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Sync 30s nginx-ingress-controller Scheduled for sync$ kubectl get endpointslices -l kubernetes.io/service-name=apiNAME ADDRESSTYPE PORTS ENDPOINTS AGEapi-abc12 IPv4 80 10.244.1.7 6d
Takeaway
Ingress is HTTP routing in front of Services. The controller is the real data plane; the Ingress object is the config.