Mainline

KAI Road of Kubernetes 03 — What a Service is, and why Pod IPs should not be your interface

Pods can be replaced by Deployments at any time. A Service gives a changing group of Pods a stable network entry point through DNS, ClusterIP, selectors, EndpointSlices, and ready backend endpoints.

EN Mainline Part 3
Core idea

Pods change and Pod IPs drift. A Service turns a group of Pods into a stable network entry point.

Takeaway

When debugging a Service, do not stop at the ClusterIP. Follow selector, EndpointSlice, Pod readiness, and targetPort.

The previous chapter introduced Deployment as the reason we do not babysit Pods manually.

A Pod can fail and be recreated. A rollout can replace old Pods with new ones. A node problem can move the workload somewhere else.

That is normal.

But it immediately creates the next problem:

If Pods keep changing, how does anything connect to them?

I would not let a frontend remember a backend Pod IP. That is like asking customers to chase whichever worker happens to be on shift today.

The sentence I would keep is this:

Pods change and Pod IPs drift. A Service turns a group of Pods into a stable network entry point.

Do not chase Pod IPs

A Pod has its own IP address. That matters.

But a Pod IP is not a durable interface.

A Deployment might have three Pods now and three different Pods after the next rollout. Names change. IPs change. Nodes can change.

If another workload connects directly to a Pod IP, it is binding itself to one temporary landing of the workload.

Kubernetes wants a different mental model.

You put a changing group of Pods behind a stable entry point, so the caller only needs to know:

I want the web service.

not:

I want Pod 10.244.2.17.

That is where Service enters.

KAI notebook style diagram showing a stable Kubernetes Service counter in front of changing backend Pods selected by labels.
A Service does not make one Pod stable. It gives a stable entry point to a replaceable set of Pods, so clients do not need to chase Pod IPs.

Think of Service as a fixed counter

Continue the shift-schedule example from the Deployment chapter.

A Deployment is like the schedule plus the manager: keep three people at the counter, replace someone who leaves, and roll out a new process gradually.

What is a Service?

I think of it as the fixed counter number.

Customers do not need to know which three people are working behind the counter today. They do not need to know where one worker moved.

They only need to know:

Go to counter 3.

That is what a Service does.

It is not the worker. It is not the shift schedule. It is the stable entry point in front of a set of matching, currently usable backend Pods.

This analogy also prevents one common mistake:

If there is nobody behind the counter, the counter number still exists, but the service will not magically work.

A Service can have an entry point and still have no healthy backends.

What a Service actually describes

A common Service looks like this:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080

I read it like this:

  • metadata.name: web: the stable entry point is named web
  • type: ClusterIP: by default, expose a stable internal cluster IP
  • selector: app=web: which Pods count as backends for this Service
  • port: 80: the port clients use on the Service
  • targetPort: 8080: the port the application listens on inside the Pod

The easy-to-miss part is the selector.

A Service does not find Pods by Deployment name. It does not find Pods by Pod name.

It usually finds backends by label selector.

So if the Deployment’s Pod template uses:

labels:
  app: api

but the Service says:

selector:
  app: web

then the Service may have no backends.

The entry point exists, but the room behind the door is empty.

How Service, DNS, and EndpointSlice fit together

A normal ClusterIP Service gets a cluster-internal IP address and a DNS name.

Within the same namespace, you can often use the short name:

web

Across namespaces, you can be explicit:

web.default.svc.cluster.local

But DNS only helps the client find the Service entry point.

The current backend list is not hidden inside DNS. Kubernetes maintains it through EndpointSlices based on the Service selector.

A useful path is:

client -> Service DNS / ClusterIP -> EndpointSlice -> ready Pod IP:targetPort

A fuller version:

  1. You create a Service with a selector.
  2. Kubernetes finds Pods whose labels match the selector.
  3. EndpointSlices record the backend endpoints.
  4. The service proxy or dataplane on each node routes traffic using that endpoint information.
  5. Clients keep using the stable Service name or ClusterIP.
KAI notebook style diagram showing a client calling Service DNS and ClusterIP, selector mapping to EndpointSlices, and traffic routed to ready backend Pods.
The Service gives clients a stable address. EndpointSlices track the changing backend endpoints. In normal cases, Pods that are not ready or are terminating should not be treated as regular usable backends.

One detail matters: Service is not the health check itself.

Whether a Pod becomes a usable backend is usually tied to Pod readiness. A Pod can exist, but if readiness has not passed, traffic should not normally be sent to it.

So when debugging a Service, do not only ask:

Does the Service have an IP?

Ask:

Does the EndpointSlice have ready endpoints?

Do not mix up port and targetPort

This mistake is very common.

The Service port is what the client sees.

The targetPort is where the traffic is sent on the backend Pod.

For example:

ports:
  - name: http
    port: 80
    targetPort: 8080

means:

client -> web:80 -> Pod:8080

If the application actually listens on 3000 but the Service says targetPort: 8080, the Service can exist and endpoints can exist, but requests still fail.

For production manifests, I usually prefer named ports where possible:

# Service
ports:
  - name: http
    port: 80
    targetPort: http

# Pod template
ports:
  - name: http
    containerPort: 8080

Then if the container port changes from 8080 to 3000 later, keeping the same semantic port name can reduce unnecessary Service changes.

Service types: know the boundary first

Beginners often think Service means “expose this to the internet.”

That is only partly true.

The default ClusterIP Service is an internal cluster entry point, not a public internet entry point.

A first-pass model:

  • ClusterIP: default, internal to the cluster
  • NodePort: opens a port on every node from a configured range; usually not my first choice as a clean production entry point
  • LoadBalancer: asks a cloud provider or external implementation to provision an external load balancer
  • ExternalName: maps the Service name to an external DNS name using CNAME

There is also headless Service:

clusterIP: None

It does not give the usual ClusterIP. DNS returns backend endpoints instead. You often see this with StatefulSets or systems where clients need to know individual backend identities.

The main point for this chapter is simpler:

Service is not a synonym for “public exposure.” It is first a stable service-entry abstraction.

Common misunderstandings

1. Thinking a Service creates Pods

A Service does not create Pods.

Creating and maintaining Pods is the Deployment / ReplicaSet layer.

A Service watches labels, finds matching backends, and provides the entry point.

If there are no Pods, or if the selector does not match any Pods, the Service will not create backends for you.

2. Assuming a Service IP means the app works

A ClusterIP means the Service object exists.

For the request to work, you still need:

  • a selector that matches Pods
  • EndpointSlices with endpoints
  • Pods that pass readiness
  • a correct targetPort
  • an application that actually responds

A Service is an entry point, not a magic guarantee.

3. Treating selectors casually

If the selector is too narrow, it selects no Pods.

If it is too broad, it may select Pods that should not receive this traffic.

This is painful because the YAML can be valid while traffic behavior is wrong.

I treat the Service selector as a contract:

Which Pods are allowed to stand behind this entry point?

4. Mixing Service, Ingress, and LoadBalancer together

Service first solves stable access to a group of Pods inside the cluster.

Ingress and Gateway more often solve how external HTTP / HTTPS traffic enters the cluster, including host/path routing, TLS, and richer traffic policy.

A LoadBalancer Service can give you an external entry point, but it is not a full replacement for Ingress or Gateway.

Separate the layers first. The rest becomes easier.

How I would inspect a Service

If someone says “this svc is not working,” I would not stop at kubectl get svc.

I follow this path:

KAI notebook style debugging map for Kubernetes Service: check Service, selector, EndpointSlice, Pod readiness, and port to targetPort mapping.
First rule of Service debugging: if there are no endpoints, do not blame DNS first. Check selector, Pod labels, readiness, and targetPort.
kubectl get svc -n <ns>
kubectl describe svc <svc-name> -n <ns>

kubectl get endpointslice -n <ns> \
  -l kubernetes.io/service-name=<svc-name>

kubectl get pods -n <ns> --show-labels
kubectl get pods -n <ns> -l app=web -o wide
kubectl describe pod <pod-name> -n <ns>

I look for:

  • what selector the Service uses
  • whether Pod labels actually match
  • whether EndpointSlices contain addresses
  • whether endpoint conditions are ready / serving
  • whether Service port maps to the correct backend targetPort
  • whether the Pod readiness probe keeps failing

To test DNS and HTTP from inside the cluster, I might start a temporary client:

kubectl run tmp-curl -n <ns> --rm -it \
  --image=curlimages/curl -- sh

nslookup web
curl -v http://web:80

If DNS resolves but the EndpointSlice is empty, the problem is usually not DNS.

If EndpointSlices have ready endpoints but requests time out, then I continue toward targetPort, application listen address, NetworkPolicy, CNI, or dataplane issues.

My Service mental model

I do not like memorizing Service as “the Kubernetes load balancer.”

That sentence is too rough. It mixes ClusterIP, NodePort, LoadBalancer, and Ingress into one blurry idea.

I prefer this:

A Service is a stable entry point backed by a backend list that changes as Pods change.

That naturally leads to two debugging questions:

  1. Does the entry point exist?
  2. Is the backend list correct and usable?

Once those are separate, Service becomes much easier to reason about.

Three takeaways

  1. Pod IP is not the stable interface. A Service is the stable entry point a group of Pods exposes.
  2. A Service uses selectors to find Pods, and EndpointSlices track the current usable backend endpoints.
  3. Debug Service through Service -> selector -> EndpointSlice -> Pod readiness -> targetPort; do not stare only at ClusterIP.

Next in the series: Service gives the cluster a stable internal entry point. How does external HTTP / HTTPS traffic enter?