Right, let’s talk about the one thing that saves your bacon in a Kubernetes cluster: the Service. You’ve deployed your app. You’ve got, say, three nginx Pods running. They all have their own unique, flaky IP addresses. Pods die, get rescheduled, and get new IPs. You can’t rely on those IPs for anything. Telling another app, “Hey, just connect to 10.244.1.5,” is a recipe for failure. It’s like trying to mail a letter to a friend who changes their apartment number every other day.

This is where the Service comes in. It’s not a Pod, it’s not a container. It’s an abstraction. A lie, but a useful one. It presents a single, stable Virtual IP (VIP) and hostname that other things can use to connect to your application. Under the hood, it’s a glorified, dynamic load balancer that automatically updates its list of targets (your Pods) as they come and go. It’s the permanent phone number for your ever-changing fleet of Pods.

How It Works: It’s All iptables (Mostly)

Don’t let the YAML fool you; the magic isn’t in the API server. When you create a Service, the Kubernetes control plane (specifically, the kube-proxy component running on every node) does the heavy lifting. It watches for Services and EndpointSlices (which track which Pod IPs belong to the Service), and then it programs the network rules on each node to make that VIP work.

Most commonly, it uses iptables rules. It doesn’t run a proxy daemon. It just sets up a bunch of DNAT rules that say, “Hey, any packet destined for this Service VIP on port 80? Yeah, I’m gonna redirect that to one of these Pod IPs on port 80 instead.” It does load balancing at the network packet level, which is brutally efficient. There’s also an IPVS mode for those with massive services, which is more efficient than iptables for large sets of backend Pods.

Here’s the most basic Service manifest. You’ll notice the selector (app: nginx); this is how the Service knows which Pods to target. It’s a label lookup.

apiVersion: v1
kind: Service
metadata:
  name: my-awesome-service
spec:
  selector:
    app: nginx # This must match the labels on your Pods!
  ports:
    - protocol: TCP
      port: 80 # The port the Service listens on
      targetPort: 80 # The port the Pods listen on

Apply this, and my-awesome-service gets a stable ClusterIP. Other Pods in the cluster can now talk to your Nginx Pods by hitting http://my-awesome-service.default.svc.cluster.local:80 or just http://my-awesome-service. The internal DNS is your friend.

The Critical Difference: port, targetPort, and nodePort

This trips everyone up, so let’s be direct:

  • port: This is the port on which the Service itself is exposed. This is the port you use to talk to the Service.
  • targetPort: This is the port on the Pod that receives the traffic. Your application container must be listening on this port. If you leave it blank, it defaults to the port value.
  • nodePort (Only for NodePort Services): This is the port opened on every node in the cluster. More on this in a minute.

Why have the distinction? Decoupling. Your Service can listen on a standard port (e.g., port: 80) while your Pods listen on some arbitrary high-numbered port (targetPort: 8080). You change your app’s port? You just change targetPort in one Service definition, not in every client that connects to it.

The Endpoints Slice: The Service’s Little Black Book

A Service doesn’t directly know about Pods. It knows about labels. The actual IP-to-Service mapping is handled by the EndpointSlice API (an evolution of the older Endpoints API). When you create a Service, a corresponding EndpointSlice controller automatically creates an EndpointSlice object and continuously updates it with the IP addresses of all Pods that match the Service’s selector.

You can (and should) check this to debug why a Service might not be routing traffic.

kubectl get endpointslices
kubectl describe endpointslice my-awesome-service-abcde

If you see no endpoints here, your Service’s selector doesn’t match any Pods. That’s your first debugging step. It’s almost always a label typo.

When Selectors Aren’t Enough: The Headless Service

Sometimes, you don’t want load balancing. You might want to talk directly to a specific Pod, or you’re running a stateful application like a database where each Pod has a unique identity. Maybe your client needs to discover all Pod IPs itself for its own logic.

You do this by setting clusterIP: None. This creates a “headless” Service. Kubernetes won’t assign it a VIP. Instead, a DNS lookup for the Service will return the IPs of all the Pods it points to.

apiVersion: v1
kind: Service
metadata:
  name: my-headless-service
spec:
  clusterIP: None # This is the magic line
  selector:
    app: stateful-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Now, a lookup for my-headless-service.default.svc.cluster.local returns multiple A records. It’s a way to bypass the Service’s load balancer while still using its discovery mechanism. StatefulSets use this heavily.