Understanding Kubernetes Services: Giving Pods a Stable Network Address
-
Ahmed Muhi - 13 Jun, 2025
Introduction
In the previous article, we built a ReplicaSet and watched it do its job. It kept the right number of pods running. When one pod disappeared, the ReplicaSet created another. When we changed the count, it scaled the pods up or down.
That solved one problem, but it also revealed the next one.
The pods were alive, but they were still changing. Each pod had its own name. Each pod had its own IP address. When a pod was deleted and replaced, the new pod came back with a new name and a new IP. That is fine for the ReplicaSet, because the ReplicaSet only cares about the number of matching pods. But it is a problem for anything that needs to send traffic to those pods.
Imagine another part of your application needs to call the frontend. Which pod should it call? Which IP address should it use? If you hardcode one pod IP, that IP might disappear the next time the pod is replaced. If you keep a list of pod IPs, that list becomes wrong as soon as the ReplicaSet scales up, scales down, or replaces a pod.
So we need another Kubernetes object.
A Service gives a changing group of pods a stable way to be reached. The pods behind it can come and go, but the Service stays in place. Other parts of the application talk to the Service, and the Service sends traffic to the pods that are currently available.
That is the next step in the series. Pods run the application. ReplicaSets keep the right number of pods running. Services give those pods a stable address.
In this article, we will look at what a Service does, how it finds pods using labels, how traffic reaches the pods behind it, and how to write a simple Service in YAML.
What a Service Solves
A ReplicaSet can keep three frontend pods running, but it does not give you one stable place to reach them.
That is the key problem.
Pods are created and removed all the time. When a pod is created, it gets its own IP address. When that pod is removed, that IP address is gone with it. If a replacement pod appears, it gets a new IP address.
So even though the application is still running, the individual pods behind it are not stable targets.
Imagine you have another pod in the cluster that needs to send a request to the frontend. Without a Service, it would need to call one of the frontend pod IPs directly.
That might work for a moment.
But then one of those frontend pods is deleted and replaced. Or the ReplicaSet scales from three pods to five. Or it scales back down again. The list of pod IPs keeps changing, and anything trying to call those pods directly has to somehow keep up.
That is not a good model.
You do not want every part of your application tracking which frontend pods exist right now. You want one stable name or address that other parts of the application can use.
That is what a Service gives you.
A Service sits in front of a group of pods and gives them a stable network identity. The pods behind the Service can change, but the Service stays the same.
So instead of another pod saying:
Call frontend-x4k2p at this pod IP
It can say:
Call the frontend Service
The Service then works out which frontend pods are available and sends traffic to one of them.
A ReplicaSet solves the “how many pods should be running?” problem.
A Service solves the “how do I reach those pods?” problem.
How a Service Finds Pods
A Service does not point to pod names.
That is important, because pod names change. If the Service depended on names like frontend-x4k2p or frontend-m8kl9, it would have the same problem we are trying to solve. The moment a pod was replaced, the Service would be pointing at something old.
Instead, a Service uses labels.
This should feel familiar from the ReplicaSet article. A ReplicaSet uses labels to decide which pods it should count. A Service also uses labels, but for a different reason.
The ReplicaSet asks:
Which pods should I keep running?
The Service asks:
Which pods should I send traffic to?
So if your frontend pods have this label:
labels:
app: frontend
Then the Service can use this selector:
selector:
app: frontend
That selector means:
Send traffic to pods labelled app: frontend.
It does not matter what the pod names are. It does not matter if one pod is deleted and another pod appears with a new name. As long as the new pod has the label app: frontend, the Service can include it.
This is why labels matter so much in Kubernetes. They are not just tags for organisation. They are how Kubernetes objects find each other.
In the ReplicaSet article, the label connected the ReplicaSet to the pods it managed. In this article, the same label connects the Service to the pods it can send traffic to.
So the same three frontend pods can be seen in two different ways:
ReplicaSet: keep these pods running.
Service: send traffic to these pods.
Same pods. Same labels. Different jobs.
A Service in YAML
Now that the idea is clear, we need to describe the Service to Kubernetes.
Here is a simple Service for the frontend pods:
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
type: ClusterIP
selector:
app: frontend
ports:
- port: 80
targetPort: 80
Read it from the top, and the shape should look familiar: apiVersion, kind, metadata, and spec.
The apiVersion is v1, the same as a Pod. The kind is Service, which tells Kubernetes that this object is here to provide network access to a group of pods.
The name is important too:
metadata:
name: frontend
This gives the Service a stable name inside the cluster. Other pods can use this name when they want to reach the frontend. They do not need to know the names or IP addresses of the frontend pods behind it.
Then comes the spec.
type: ClusterIP
ClusterIP means this Service gets an internal IP address inside the cluster. Other pods in the cluster can use it, but it is not exposed directly to the outside world. That makes it a good first Service type to learn, because it focuses only on pod-to-pod communication inside Kubernetes.
Next is the selector:
selector:
app: frontend
This is how the Service finds the pods behind it. It looks for pods with the label app: frontend. Any pod with that label can be included. If a pod disappears, it drops out. If a new pod appears with the same label, it can be included too.
Finally, we have the ports:
ports:
- port: 80
targetPort: 80
There are two ports here, and they do slightly different jobs.
port: 80 is the port exposed by the Service. This is the port other pods use when they talk to the Service.
targetPort: 80 is the port on the pods behind the Service. This is where the Service sends the traffic.
In this example, both numbers are 80, so it looks simple. But they do not have to be the same. You could expose the Service on one port and forward traffic to a different port on the pods.
So this Service says:
Create an internal Service called frontend. Send traffic that arrives on port 80 to pods labelled app: frontend, on their port 80.
That is the whole YAML. One stable name, one selector, and one rule for where traffic should go.
Watching the Service Work
Save the YAML to a file called frontend-service.yaml, then apply it to the cluster.
kubectl apply -f frontend-service.yaml
Now check the Service:
kubectl get service frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend ClusterIP 10.96.184.120 <none> 80/TCP 10s
The Service now has a stable internal IP address. In this example, that IP is 10.96.184.120.
That IP belongs to the Service, not to any one pod. The pods behind the Service can change, but the Service IP stays the same.
You can also see that the Service is a ClusterIP Service. That means it is reachable from inside the cluster, but it is not exposed directly to the outside world.
Now check the pods again:
kubectl get pods -l app=frontend
NAME READY STATUS RESTARTS AGE
frontend-m8kl9 1/1 Running 0 10m
frontend-p9j7r 1/1 Running 0 10m
frontend-k2f9x 1/1 Running 0 9m
These are the pods behind the Service. The Service finds them because they have the label app: frontend.
To test the Service, create a temporary pod inside the cluster and use it as a client:
kubectl run test-client --rm -it --image=busybox -- sh
From inside that temporary pod, call the Service by name:
wget -qO- http://frontend
You are not calling frontend-m8kl9, frontend-p9j7r, or frontend-k2f9x directly. You are calling frontend, the Service name.
That is the important shift.
The client does not need to know which frontend pod receives the request. It does not need to know the pod names. It does not need to know the pod IPs. It only needs to know the Service name.
Now delete one of the frontend pods:
kubectl delete pod frontend-m8kl9
The ReplicaSet will create a replacement. Check the pods again:
kubectl get pods -l app=frontend
NAME READY STATUS RESTARTS AGE
frontend-p9j7r 1/1 Running 0 11m
frontend-k2f9x 1/1 Running 0 10m
frontend-b7n4q 1/1 Running 0 5s
One pod is gone. A new one has appeared. The pod name changed, and the pod IP changed too.
But the Service did not change.
If you call the Service again from inside the cluster:
wget -qO- http://frontend
the request still works.
That is the Service doing its job. The pods behind it can change, but the address clients use stays the same.
Where the Traffic Goes
When you call the Service, you are not calling a pod directly.
You are sending traffic to the stable address that Kubernetes created for the Service. From there, Kubernetes forwards the request to one of the pods that matches the Service selector.
In our example, the Service is looking for pods with this label:
app: frontend
At the moment, there are three pods with that label:
frontend-p9j7r
frontend-k2f9x
frontend-b7n4q
Any of those pods can receive the request.
That is important. A Service does not mean “always send traffic to the same pod.” It means “send traffic to one of the available pods behind this Service.”
So when another pod calls:
http://frontend
Kubernetes resolves that name to the Service IP address, then sends the request to one of the frontend pods behind it.
You usually do not care which specific pod receives the request. If all three pods are running the same application, any of them should be able to answer. That is why the ReplicaSet created identical pods in the first place.
This gives you two useful things at the same time.
The client gets one stable name to call:
frontend
And the frontend pods can still change behind the scenes.
One pod can be deleted. Another can be created. The ReplicaSet can scale the group from three pods to five. The Service keeps using the selector to find the pods that match, and traffic continues to go to the pods that are available.
That is the simple mental model:
Client → Service → one of the matching pods
The client does not track pods. The Service does that job for it.
Service Types
So far, we have used a ClusterIP Service.
That is the default Service type, and it is the best one to start with because it solves the first networking problem: how pods inside the cluster reach other pods inside the cluster.
But Kubernetes has a few Service types, and each one answers a slightly different question.
ClusterIP
A ClusterIP Service gives your pods a stable address inside the cluster.
That means other pods can call the Service, but the Service is not directly exposed outside Kubernetes.
This is what we used for the frontend example:
type: ClusterIP
Use ClusterIP when one part of your application needs to talk to another part inside the same cluster.
For example:
backend pod → frontend Service → frontend pods
or:
frontend pod → api Service → api pods
The important idea is that the traffic starts inside the cluster and stays inside the cluster.
NodePort
A NodePort Service exposes the Service through a port on each worker node.
That means Kubernetes opens a port on the nodes, and traffic sent to that node port can reach the Service.
For example, Kubernetes might expose your Service on a port like this:
<NodeIP>:30080
That can be useful when you are learning or testing, because it gives you a quick way to reach a Service from outside the cluster.
But it is not usually the cleanest way to expose an application properly. You have to think about node IPs, high port numbers, firewalls, and what happens if nodes change.
So NodePort is useful to understand, but it is usually not where the story ends.
LoadBalancer
A LoadBalancer Service asks the platform running Kubernetes to create an external load balancer for you.
This is common in managed Kubernetes platforms like AKS, EKS, and GKE. You create a Service with:
type: LoadBalancer
and the cloud provider creates an external IP address that can send traffic into your Service.
That makes LoadBalancer useful when you want something outside the cluster to reach your application.
The simple idea is:
External user → cloud load balancer → Kubernetes Service → pods
The Simple Way to Think About Them
For now, keep the three types in your head like this:
ClusterIP: reachable inside the cluster
NodePort: reachable through a port on each node
LoadBalancer: reachable through an external load balancer
In this article, we are focusing on ClusterIP because it teaches the core idea of Services without adding outside traffic yet.
A Service gives pods a stable address.
Once that idea is clear, the next question becomes: how do we expose an application properly to users outside the cluster, especially for HTTP and HTTPS traffic?
That is where Ingress comes in.
Where This Leads
You now have the next piece of the Kubernetes picture.
A pod runs your application. A ReplicaSet keeps the right number of those pods running. A Service gives those pods a stable way to be reached.
That separation matters.
The ReplicaSet does not give you a stable address. It only makes sure the right number of pods exist. The Service does not create or replace pods. It only finds the pods that match its selector and sends traffic to them.
Together, they solve two different problems:
ReplicaSet: keep the pods running
Service: give the pods a stable address
That is why the same pods can sit behind both objects. The ReplicaSet looks at the pods and asks, “Do I have enough of these?” The Service looks at the pods and asks, “Can I send traffic to these?”
Both use labels to find the right pods, but they use them for different jobs.
In this article, we focused mostly on ClusterIP, which gives pods a stable address inside the cluster. That is the first networking step. It lets one part of your application talk to another part without tracking pod names or pod IP addresses.
But users outside the cluster still need a clean way to reach your application.
You could expose a Service with NodePort or LoadBalancer, and sometimes that is enough. But for web applications, especially when you care about HTTP, HTTPS, hostnames, paths, and routing rules, Kubernetes has another object that sits above Services.
That object is called Ingress.
That is where the next article goes. Services give pods a stable address inside Kubernetes. Ingress gives external HTTP and HTTPS traffic a clean way into those Services.