Module 2: Pods Deployments And Services
6. Services: `ClusterIP`, `NodePort`, and why a Pod isn't a stable address
Description
Lesson 5 left andes-cargo-status-api running with two replicas — but if you wanted to curl it right now, you'd have a real problem: which of the two IPs do you talk to? What happens if you delete a Pod, like you did in lesson 5, and its replacement arrives with a different IP? This lesson resolves exactly that problem with this module's third central object: a Service, the piece that gives you an address that never changes, no matter how many times the Pods behind it rotate.
Connection to the module
This lesson is the last conceptual piece before lesson 7 puts it into real practice. Everything you build from here on in this guide — Ingress in Module 4, NetworkPolicy in that same module, all the way to the AWS Load Balancer Controller in Module 7 — exists on top of this same concept: a Service is the basic unit of discovery inside a Kubernetes cluster, the piece that makes it possible for two parts of a system to talk to each other without knowing each other's IP.
The exact problem: Pod IPs are ephemeral
You already confirmed this with your own hands in lesson 5: when you deleted andes-cargo-status-api-56856576d4-8lb5t (IP 10.244.1.2), its replacement arrived with a different IP (10.244.1.3). This isn't an implementation detail — it's an explicit guarantee of Kubernetes' model: a Pod's IP is only valid while that specific Pod exists. As soon as it's recreated — for any reason: you deleted it, the node failed, a RollingUpdate replaced it — the old IP stops making sense, and the new one is, in principle, unpredictable.
This makes "a Pod's IP" the worst possible address for anything else (another service, an external client, your own browser) to use directly. You need something stable — a name, an address, that doesn't change no matter how many times the ReplicaSet replaces Pods underneath. That "something" is a Service.
THE PROBLEM: TWO PODS, TWO ADDRESSES THAT CHANGE
Moment 1 Moment 2 (after a delete)
Pod-8lb5t 10.244.1.2 Pod-8lb5t ── no longer exists
Pod-vjc9k 10.244.2.4 Pod-7ztk9 10.244.1.3 ← new IP
Pod-vjc9k 10.244.2.4 ← unchanged
Which IP does a client that needs "the status-api service" talk to?
Neither is a safe or permanent answer.
How a Service resolves this: by label, not by Pod name
A Service doesn't point to specific Pods by name — it points to any Pod that has a given label, the same metadata.labels mechanism you already used on every Pod and Deployment in this module. When you declare a Service with selector: app: andes-cargo-status-api, Kubernetes automatically and continuously maintains an updated list of the IPs of every Pod that at that moment has that label — with no need for you to tell it every time a Pod is born or dies.
HOW A Service FINDS ITS PODS
Service: status-api-service
selector: app=andes-cargo-status-api
│
│ Kubernetes constantly looks for every Pod with that label
▼
┌─────────────────────────────────────────────┐
│ Pod (app=andes-cargo-status-api) 10.244.1.3 │ ← this list updates
│ Pod (app=andes-cargo-status-api) 10.244.2.4 │ itself, every time
└─────────────────────────────────────────────┘ a Pod is born or dies
That updated IP list has its own name inside Kubernetes: an Endpoints object (or, in recent versions, EndpointSlice), which the cluster itself maintains for you — you never declare it by hand, but you are going to inspect it with kubectl get endpoints in lesson 7, to confirm the Service really "sees" your two Pods.
The Service itself, meanwhile, exposes a single fixed IP address (ClusterIP, see the next section) and a DNS name resolvable inside the cluster — status-api-service.andes-cargo.svc.cluster.local, following the <service-name>.<namespace>.svc.cluster.local pattern that CoreDNS (the internal name server, already named in Module 1, lesson 5) resolves automatically. When a client talks to that address or that name, something inside the cluster (kube-proxy, a component running on each node) routes that connection to one of the IPs in the Endpoints list, spreading traffic across all the available ones. That spreading is, literally, the basic load balancing you're going to confirm with real evidence in lesson 8.
The Service types, and when to use each one
Kubernetes offers several Service types, each one resolving a different scope of "who can reach this stable address":
| Type | Scope | When to use it |
|---|---|---|
ClusterIP (the one this guide uses) | Only reachable inside the cluster — other Pods, or another process talking to kube-apiserver via kubectl port-forward | The default, and the right choice for an internal service that doesn't need direct traffic from outside the cluster — exactly status-api-service's case until Module 4 adds an Ingress to it |
NodePort | Opens a fixed port (in the 30000-32767 range) on every node in the cluster, reachable from outside with <any-node's-IP>:<port> | Useful for quick tests or labs with no external load balancer; uncommon in a real production cluster, because it directly exposes the nodes' IPs |
LoadBalancer | Asks the underlying cloud provider (AWS, GCP, Azure) to provision a real load balancer, with its own public IP | The type you'd use on real EKS to expose a service directly to the internet with no Ingress — not applicable on kind, because there's no cloud provider underneath that knows how to create that balancer; Module 4 solves the same problem with Ingress + ingress-nginx instead |
ExternalName | Doesn't route to any Pod — maps the Service's name to an external DNS name | A special, uncommon case, out of this guide's scope |
This guide uses ClusterIP for status-api-service in this lesson and in lesson 7, and stays with that type for the rest of the guide — even after Module 4, when Ingress exposes the service to external traffic. This isn't a temporary limitation: it's the real production pattern. Ingress doesn't replace the Service, it sits in front of it — the Service stays ClusterIP, and it's Ingress that receives external traffic and forwards it inward. You're going to see exactly this, with real evidence, in Module 4.
A Service's minimal anatomy
apiVersion: v1
kind: Service
metadata:
name: <service-name>
namespace: <namespace>
spec:
type: ClusterIP
selector:
app: <label>
ports:
- port: <service-port>
targetPort: <container-port>
protocol: TCP
Two fields deserve attention before lesson 7:
spec.selector— exactly the same labeling mechanism you already know from theDeployment(lesson 3) and the Pod (lesson 2). It must match theDeployment'sspec.template.metadata.labelswhose Pods you want to expose — if even one character doesn't match, theServiceexists, but finds no Pod, and any traffic it receives has nowhere to go.spec.ports[].portversustargetPort— two numbers that are almost always confused the first time they're seen together.portis the port the Service listens on (the one any client uses to talk to it);targetPortis the port the container inside the Pod really listens on (the samecontainerPortyou declared on theDeployment). They can be the same number, but they don't have to be — lesson 7 usesport: 80withtargetPort: 8080, precisely to show they're independent.
Analogy: the switchboard, not an employee's direct line
Picking back up the office building analogy for the last time in this module: if you gave a client a specific employee's direct phone number (the equivalent of "a Pod's IP"), that number would stop working the moment that employee changed desks, left the company, or simply sat at a different desk one day. A switchboard, by contrast, has a fixed extension number — say, "dial 100 for Customer Service" — that never changes, no matter who's sitting behind that desk today. Whoever calls never needs to know who answers; they only need the extension number. That's exactly what a Service gives to anyone who needs to talk to andes-cargo-status-api: a fixed extension, status-api-service, behind which as many Pods as the Deployment decides can rotate, with the caller noticing no difference whatsoever.
Common mistakes
Assuming a Service is a process running somewhere, like one more Pod (conceptual). What happens: someone looks for a Service with kubectl get pods, expecting to find some process running behind that stable address, in addition to andes-cargo-status-api's real Pods. Why it happens: the word "Service" sounds like "something running," like any other piece of software. How to spot it: if you look, unsuccessfully, for something called status-api-service in the Pod list. How to fix it: a Service is, mainly, a networking rule maintained by kube-proxy on each node (iptables or IPVS rules, depending on the cluster's configuration) — there's no Pod or container running "as" the Service itself. It's an address and a routing rule, not a process.
Forgetting that port and targetPort are independent, and using them as if they were the same number out of habit (configuration). What happens: someone copies an example where port and targetPort match, and assumes they always have to be equal — then gets confused when they need to expose a Service on port 80 (the HTTP standard, no need to specify it in the URL) while the real container listens on a different port, like 8080. How to spot it: if you try to connect to a Service on one port and get "connection refused," even though the Pod behind it is healthy — check whether targetPort matches the real port the container exposes (containerPort on the Deployment), not the port you're talking to the Service on. How to fix it: lesson 7 of this module uses exactly this case — port: 80, targetPort: 8080 — so the distinction gets installed with a real example, not just in theory.
Expecting LoadBalancer to work on kind the same way it does on a real cloud cluster (platform, a common misunderstanding for anyone learning with kind for the first time). What happens: someone declares a LoadBalancer-type Service, expecting to get an automatically assigned external IP, as would happen on EKS, GKE, or AKS — and on kind that IP stays permanently in <pending> state, never resolving. Why it happens: LoadBalancer depends on the underlying cloud provider implementing the logic to provision a real balancer; kind runs on Docker on your machine, with no cloud provider fulfilling that contract. How to spot it: kubectl get service shows EXTERNAL-IP: <pending> indefinitely for a LoadBalancer-type Service. How to fix it: this guide never uses LoadBalancer against kind for this exact reason — Module 4 solves the same problem (external traffic into the cluster) with Ingress + ingress-nginx, a pattern that does work fully on kind, and Module 7 explains, with full honesty, what changes when the cluster is real EKS and LoadBalancer really does provision a real ALB balancer.
Exercises
Exercise 1 — Explain the selection mechanism without the diagram. Without going back to the corresponding section, explain in your own words how a Service knows which Pods it should route traffic to, and what automatically happens when a Pod with the correct label is born or dies.
See solution
A Service doesn't point to Pods by name — it points to any Pod whose label matches its selector. Kubernetes automatically and continuously maintains a list of the IPs of every Pod that currently has that label (the Endpoints/EndpointSlice object); when a new Pod is born with the correct label, it gets added to that list on its own; when a Pod with that label dies, it gets removed on its own. The Service, meanwhile, never changes its own address — only the list of Pods it routes to underneath changes.
Exercise 2 — Choose the correct type. For each of the following three cases, state which Service type (ClusterIP, NodePort, or LoadBalancer) would be most appropriate, and why: (a) an internal service only another Pod in the same cluster needs to reach; (b) a lab service you need to quickly test from your machine, with no additional controller installed; (c) a production service on real EKS that must receive direct traffic from the internet.
See solution
(a) ClusterIP — it's exactly the case it exists for: reachable only inside the cluster, with nothing unnecessarily exposed outward. (b) NodePort — opens a fixed port on every node, directly reachable from outside with no need for Ingress or an external load balancer, though with the security and port-range limitations that make it uncommon in real production. (c) LoadBalancer — on a real cloud provider like AWS, this type asks the provider to provision a real balancer (an ALB or NLB, Module 7's topic) with its own public IP, the expected pattern for external production traffic.
Exercise 3 — Diagnose a Service with no traffic. A Service with selector: app=my-api isn't routing any traffic, even though there are running, healthy Pods in the namespace. You check the Deployment and see its Pods have the label app: myapi (no hyphen). What's the problem, and how would you confirm it with a command you already know from this module?
See solution
The problem is the labels don't match exactly: the Service looks for app=my-api (with hyphen), but the Pods have app=myapi (no hyphen) — for Kubernetes, these are two completely different labels, even though they look alike at a glance. The Service exists, it's healthy, but it finds no matching Pod, so it has nowhere to route. You'd confirm it with kubectl get endpoints <service-name> -n <namespace> (mentioned in this lesson, used with real evidence in lesson 7) — if the ENDPOINTS list shows up empty, that's direct confirmation the selector isn't finding any Pod.
Summary and next step
In this lesson you understood the exact problem a Service solves: Pod IPs are ephemeral, they change every time a Pod is recreated, so you need a stable address that doesn't depend on any individual Pod. You saw how a Service finds its Pods by label — the same metadata.labels mechanism from this entire guide — the four main types (ClusterIP, NodePort, LoadBalancer, ExternalName) and when to use each one, and why this guide uses ClusterIP consistently, even after Ingress enters the picture in Module 4.
Before moving on you should be able to: explain why a client should never depend directly on a Pod's IP; distinguish port from targetPort without confusing them; and justify why LoadBalancer doesn't work on kind the same way it does on a real cloud cluster.
Next lesson: hands-on, exposing status-api-service. There you declare the real Service, under the same name aws-serverless-and-containers-guide left documented on ECS, and really curl it — the first time, in this entire guide, that you talk to andes-cargo-status-api through an address that doesn't depend on any specific Pod.
Resources
- Kubernetes — Service — the complete official reference, including the four types developed in this lesson.
- Kubernetes — DNS for Services and Pods — official reference for the
<service>.<namespace>.svc.cluster.localnaming pattern, mentioned in this lesson. - Kubernetes — Connecting Applications with Services — official step-by-step tutorial on this same label-selection mechanism developed here.