open atlas
↑ Back to track
Deployment & Infra DEP · 10 · 01

Services and ingress

Pods are ephemeral with changing IPs, so you reach them through a Service: a stable virtual IP that load-balances over pods matching a label selector, with Ingress as the shared HTTP router in front. Selector mismatch means zero endpoints.

DEP Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A rollout goes green. Pods are Running, the Deployment reports all replicas ready, and yet every request to the app hangs and then times out. kubectl get endpoints api prints a single chilling line: api <none>. The Service is pointing at nothing. Someone renamed the pod label from app: api to app: api-svc and never touched the Service’s selector — so the Service matches zero pods, builds an empty endpoint list, and quietly black-holes traffic. The pods are fine. The wiring between them and the Service is broken.

Why you can’t address a pod directly

When you deploy to Kubernetes, the first instinct is to grab the pod’s IP and call it directly — and that instinct is exactly what gets you into the failure in the hook above. Here is why it can never work reliably.

A pod is the cheapest, most disposable thing in Kubernetes. The scheduler kills it, reschedules it, scales it up and down, and moves it to another node on a whim — and every time it comes back it gets a new IP from the pod network. There is no stable address you can hardcode, no DNS name that survives a rollout, no guarantee the IP you resolved a second ago still belongs to a live pod. Directly dialing a pod IP is a bug waiting for the next reschedule.

The Service is the fix. It is an API object that gives a set of pods one stable identity: a virtual IP (the ClusterIP) and a DNS name (<service>.<namespace>.svc.cluster.local) that live for the lifetime of the Service, independent of which pods exist behind it right now. Clients talk to the Service; the Service load-balances each connection across the healthy pods. Pods churn underneath; the address in front never moves.

The selector-to-endpoints mechanism

The link between a Service and its pods is labels, not IPs. A Service declares a selector — a set of label key/values — and Kubernetes continuously watches for pods that (a) match every label in the selector and (b) are Ready (passing their readiness probe). For each such pod it records the pod IP and port into an EndpointSlice (the modern, sharded replacement for the older single Endpoints object). That EndpointSlice is the load-balancing target list; kube-proxy on every node programs the data plane (iptables or IPVS rules) from it.

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api            # must match the pods' labels exactly
  ports:
    - name: http
      port: 80          # the Service's virtual port
      targetPort: 8080  # the container port on the pod

Two things follow from this. First, the matching is dynamic: scale the Deployment up and new Ready pods are added to the slice within moments; a pod that fails its readiness probe is removed, so traffic stops going to it without you deleting anything. Second — and this is the failure that bites everyone — if the selector matches no Ready pods, the EndpointSlice is empty, and the Service routes to nowhere. It still has a ClusterIP, still resolves in DNS, still accepts connections — and then refuses or times out, because there is no backend to forward to.

Why this works

“Ready” is doing real work here. A pod can be Running but not Ready — it is in the EndpointSlice only once its readiness probe passes. This is deliberate: during a rolling update, new pods are kept out of the Service until they can actually serve, and old pods are pulled from the slice before termination. Get the readiness probe wrong and you can ship a Service that has running pods but zero endpoints — same black hole, subtler cause.

Service types: how far the address reaches

Every Service has a type that decides who can reach it. They stack: each builds on the one before.

TypeScopeExternal?CostUse case
ClusterIP (default)Internal virtual IP onlyNoFreeService-to-service inside the cluster
NodePortA port (30000–32767) on every nodeYes, crudeFreeBasic external access, dev, behind your own LB
LoadBalancerProvisions a cloud load balancerYesOne paid LB per ServiceA single L4 entry point for one Service
Headless (clusterIP: None)DNS returns pod IPs, no virtual IPNoFreeStatefulSets, per-pod addressing, client-side LB
Ingress (not a Service type)L7 HTTP(S) router over many ServicesYesOne LB shared by all routesHost/path routing + TLS for the whole cluster

Together these types form a single decision: how far outside the cluster do you need the traffic to reach? Start with ClusterIP for everything internal; reach for Ingress (not LoadBalancer) the moment you need HTTP routing to the public internet.

ClusterIP is the default and the workhorse — it is how your API talks to your database talks to your cache, all by DNS name, all internal. NodePort opens the same port on every node and forwards it to the Service; it is the crude knob for getting traffic in, useful in dev or when you front it with your own load balancer, but you would not hand a raw node-ip:31000 URL to users. LoadBalancer asks the cloud provider to provision a real external load balancer pointing at the Service — clean, but you pay for and manage one LB per Service, which gets expensive and unwieldy fast. Headless opts out of the virtual IP entirely: DNS returns the individual pod IPs, which is exactly what a StatefulSet (database, broker) needs so clients can address a specific replica.

Ingress: one front door for many Services

If LoadBalancer gives you one cloud LB per Service, ten public services cost ten load balancers — and you still have no HTTP-aware routing. Ingress solves both. It is an L7 (HTTP/HTTPS) router that sits in front of your ClusterIP Services and dispatches requests by host and path to the right Service, terminates TLS centrally, and does it all behind a single shared load balancer. One entry point, many backends, far cheaper than an LB per service.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: site
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api      # routes /api -> the api Service
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web      # routes everything else -> the web Service
                port:
                  number: 80

The crucial catch: the Ingress object is just rules. It does nothing on its own. You must run an ingress controller — nginx, Traefik, HAProxy, a cloud provider’s — which actually watches Ingress objects and turns them into a running reverse proxy. No controller, no routing; the YAML applies cleanly and traffic still goes nowhere. The ingressClassName tells Kubernetes which controller owns this Ingress.

Pick the best fit

You have eight public HTTP services that need host/path routing and TLS. How do you expose them?

The classic failure: zero endpoints

Return to the hook. The number-one Kubernetes networking bug is a Service whose selector does not match its pods’ labels. Nothing errors at apply time — the Service is valid YAML, gets a ClusterIP, resolves in DNS. But its selector matches no Ready pods, so the EndpointSlice is empty, and every connection is refused or times out. The same empty-endpoints symptom also comes from pods that match the labels but are not Ready (a failing or too-strict readiness probe), or a targetPort that doesn’t match the container’s actual port.

The diagnosis is always the same one command: kubectl get endpoints <svc> (or kubectl get endpointslices). If it shows <none> or no addresses, the Service is talking to no one — stop debugging the app and go check that the Service’s selector exactly equals the pods’ labels and that the pods are Ready.

Quiz

A Service has a ClusterIP and resolves in DNS, but every request times out. kubectl get endpoints shows <none>. The pods are Running. What is the most likely cause?

Recall before you leave
  1. 01
    Walk through the selector-to-endpoints mechanism: how does a Service decide which pods to load-balance over, and what makes the endpoint list empty?
  2. 02
    Why use an Ingress instead of giving each service its own LoadBalancer, and what must be running for an Ingress to actually work?
Recap

Pods are ephemeral and get a new IP on every reschedule, so you never address them directly — you put a Service in front. Now when you see kubectl get endpoints <svc> print <none>, you know exactly where to look: the selector, the labels, and the readiness probe — in that order. A Service is an API object with a stable ClusterIP and DNS name that load-balances over a set of pods chosen by a label selector: Kubernetes records the IPs of pods that match the selector AND are Ready into an EndpointSlice, and kube-proxy routes Service traffic across that live list. The type controls reach: ClusterIP is internal-only service-to-service, NodePort opens a port on every node for crude external access, LoadBalancer provisions one paid cloud LB per Service, and a headless Service drops the virtual IP so DNS returns pod IPs directly for StatefulSets. Because an LB-per-service gets expensive and offers no HTTP routing, an Ingress sits in front as a single L7 front door: it routes by host and path to many ClusterIP Services and terminates TLS centrally behind one shared load balancer — but it is only rules, and needs an ingress controller (nginx, Traefik) running to enforce them. The failure that bites hardest is the simplest: if the Service’s selector doesn’t match the pods’ labels (or the pods aren’t Ready), the EndpointSlice is empty, the Service still resolves but routes to nothing, and requests silently time out — so the first thing to check is always kubectl get endpoints.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.