This is Level 3 of our Kubernetes networking series. In Level 2, we traced how Pod-to-Pod communication actually works — and ended on a serious problem: Pod IPs are dynamic and temporary. An application can't reliably hardcode a Pod IP if that Pod might be replaced at any moment.
This article solves that exact problem by introducing one of the most important objects in Kubernetes: the Service.
Table of Contents
- The Problem: Pod IPs Aren't Reliable
- The Solution: Service
- The Core Mental Model
- How a Service Finds Its Pods: Labels and Selectors
- A Minimal Service Example
- port vs targetPort
- What a Service Actually Gives You: ClusterIP
- Visualizing Service Selection
- A Service Does Not Create Pods
- Hands-On: Deploying Backend Pods
- Hands-On: Creating the Service
- Where Does the Traffic Actually Go?
- A Service Isn't a Normal Server
- EndpointSlices: Tracking Live Backends
- What Happens When a Pod Dies
- Kubernetes Service Types
- Service vs Pod vs Deployment
- Services Don't Mean DNS — Yet
- The Complete Picture So Far
- Hands-On Investigation
- How Does the ClusterIP Actually Work?
- Level 3 Checkpoint
- What's Next: Service Discovery and DNS
The Problem: Pod IPs Aren't Reliable
Imagine three backend Pods:
backend-pod-1: 10.244.1.5
backend-pod-2: 10.244.2.8
backend-pod-3: 10.244.3.2
Your frontend wants to call the backend, so it could just call 10.244.1.5:8080 directly. But what happens when backend-pod-1 crashes? Kubernetes creates a replacement — say, backend-pod-4 at 10.244.4.9 — and the old IP is simply gone.
That means this is a fragile design:
Frontend → Pod IP
We need something more stable than a raw Pod IP.
The Solution: Service
Kubernetes solves this with a Service — think of it as a stable front door for a group of Pods.
Service
10.96.0.10
|
┌──────────┼──────────┐
↓ ↓ ↓
Pod 1 Pod 2 Pod 3
10.244.1.5 10.244.2.8 10.244.3.2
The frontend never needs to know individual Pod IPs — it just talks to 10.96.0.10, and the Service takes care of routing traffic to an appropriate backend Pod.
The Core Mental Model
This is the single most important idea in this article:
Pod IP → temporary
Service IP → stable
Pods can come and go freely. The Service gives applications a consistent, stable endpoint regardless of what's happening underneath.
How a Service Finds Its Pods: Labels and Selectors
A Service doesn't magically know which Pods belong to it — it uses labels and selectors.
Suppose your Pods are labeled:
labels:
app: backend
Then your Service can declare:
selector:
app: backend
Which effectively means: "Send traffic to any Pod labeled app: backend."
A Minimal Service Example
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 8080
Don't worry about every field yet — focus on two things:
selector:
app: backend
and:
port: 80
targetPort: 8080
port vs targetPort
This distinction trips up a lot of beginners, so let's be precise.
Suppose the Service listens on port: 80, but the Pod's container actually listens on 8080. The flow looks like this:
Client
|
| :80
↓
Service
|
| :8080
↓
Pod
In short:
-
port→ the port exposed by the Service -
targetPort→ the port the traffic is forwarded to on the Pod
So this configuration:
ports:
- port: 80
targetPort: 8080
means: Service :80 forwards to Pod :8080.
What a Service Actually Gives You: ClusterIP
A standard Kubernetes Service is assigned a stable virtual IP called a ClusterIP. For example:
Service: backend
ClusterIP: 10.96.0.10
Frontend Pod
|
| 10.96.0.10:80
↓
Service
|
↓
Backend Pod
Important nuance: the Service IP isn't a network interface living inside some specific Pod. It's a virtual IP handled entirely by the Kubernetes networking dataplane — we'll unpack exactly how that works in Level 8.
Visualizing Service Selection
Service
backend:80
10.96.0.10
|
selector: app=backend
|
┌──────────┼──────────┐
↓ ↓ ↓
Pod A Pod B Pod C
app=backend app=backend app=backend
Any Pod that doesn't match the selector is simply excluded:
Service
|
┌───────────┼───────────┐
↓ ↓ ↓
backend backend backend
✓ ✓ ✓
frontend
✗
A Service Does Not Create Pods
This is a very common point of confusion. A Service never creates or manages Pods.
Deployment → creates/manages Pods
Service → provides stable networking to those Pods
Put another way:
- Deployment says: "I need 3 backend Pods running."
- Service says: "Give me a stable way to reach those backend Pods."
They solve two entirely different problems and are designed to work together.
Hands-On: Deploying Backend Pods
For real, learnable infrastructure, a Deployment is a much better way to create backend Pods than creating them manually:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
Apply it:
kubectl apply -f backend.yaml
Check the results:
kubectl get pods -o wide
NAME IP
backend-xxxxx-aaa 10.244.1.5
backend-xxxxx-bbb 10.244.2.7
backend-xxxxx-ccc 10.244.2.9
Your actual Pod names and IPs will differ — that's expected.
Hands-On: Creating the Service
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 80
Apply it:
kubectl apply -f backend-service.yaml
Check it:
kubectl get svc
NAME TYPE CLUSTER-IP PORT(S)
backend ClusterIP 10.96.120.50 80/TCP
That 10.96.120.50 is your Service's ClusterIP.
Where Does the Traffic Actually Go?
Suppose the frontend sends a request to 10.96.120.50:80. The Service currently has three backend Pods:
10.244.1.5:80
10.244.2.7:80
10.244.2.9:80
Conceptually:
Frontend
|
| 10.96.120.50:80
↓
Service
|
├────→ 10.244.1.5:80
├────→ 10.244.2.7:80
└────→ 10.244.2.9:80
Traffic gets distributed among the currently available backend endpoints. We'll cover the exact packet-processing mechanism (kube-proxy, iptables, IPVS, eBPF) in Level 8.
A Service Isn't a Normal Server
It's tempting to imagine 10.96.120.50 as some machine sitting somewhere with its own network interface — but that's usually the wrong mental model. Think of it instead as:
Service = stable virtual endpoint + rules for reaching backend Pods
So 10.96.120.50 is a virtual Service IP, not a physical destination.
EndpointSlices: Tracking Live Backends
If a Service selects Pod A, Pod B, and Pod C, Kubernetes needs a way to track the actual current backend endpoints. Modern Kubernetes does this using EndpointSlices:
Service
↓
EndpointSlice
├── Pod A IP
├── Pod B IP
└── Pod C IP
Check them with:
kubectl get endpointslices
kubectl describe endpointslice <name>
The Service gives you a stable identity; the EndpointSlice reflects the current, real-time set of backend Pods.
What Happens When a Pod Dies
Suppose the Service currently has Pod A, Pod B, and Pod C as backends, and Pod B crashes. Kubernetes detects this and removes it from the available backends:
Before:
Service
├── A
├── B
└── C
After:
Service
├── A
└── C
A replacement Pod (Pod D) may appear shortly after:
Service
├── A
├── C
└── D
Crucially, the frontend doesn't need to change anything — it keeps using the Service name or ClusterIP (backend / 10.96.120.50) the entire time. This self-healing stability is the whole point of the Service abstraction.
Kubernetes Service Types
Kubernetes offers several Service types, each solving a different exposure problem.
ClusterIP
The default type — provides a stable internal endpoint used for communication within the cluster:
type: ClusterIP
Pod A → Service → Pod B
NodePort
Exposes the Service on a fixed port across every Node in the cluster:
type: NodePort
Client
|
↓
Node IP :30080
|
↓
Service
|
↓
Pod
For example, if a Node's IP is 192.168.1.10 and the NodePort is 30080, you can reach the Service via 192.168.1.10:30080, and Kubernetes forwards that traffic to the backend Pods.
LoadBalancer
Common in cloud environments, this type provisions an external load balancer that routes into your Service:
type: LoadBalancer
Internet
|
↓
Public IP
|
↓
Load Balancer
|
↓
Service
|
├──→ Pod
├──→ Pod
└──→ Pod
The exact provisioning mechanism depends on your cloud provider or Kubernetes environment.
ExternalName
A special type that doesn't select Pods at all — instead, it provides a DNS alias pointing to an external hostname:
type: ExternalName
Kubernetes Service name → external.example.com
This one is less central to networking fundamentals, but useful to know exists.
Quick Reference
| Type | Basic Purpose |
|---|---|
| ClusterIP | Internal stable endpoint |
| NodePort | Expose through a Node port |
| LoadBalancer | External/cloud load balancer |
| ExternalName | DNS alias to an external service |
For now, focus most of your attention on ClusterIP — everything else builds on the same underlying concept.
Service vs Pod vs Deployment
Two classic interview questions, answered cleanly.
Service vs Pod:
Pod → runs the application, has a Pod IP
Service → stable networking endpoint, selects backend Pods
In short: Pod = workload, Service = networking abstraction.
Service vs Deployment:
Deployment → manages the desired number of Pods
Service → provides stable access to those Pods
Deployment
|
├── Pod A
├── Pod B
└── Pod C
↑
|
Service
Services Don't Mean DNS — Yet
You might be wondering: "If I have a Service called backend, can I just use the name backend instead of its IP?"
Yes — eventually. But that's a separate mechanism (DNS), covered in the next article. At this level, focus purely on:
Service → stable virtual endpoint
In Level 4, we'll add the missing piece:
Service name → DNS → Service IP
Don't conflate the two just yet — understanding them separately will make DNS click much faster later.
The Complete Picture So Far
Kubernetes Cluster
┌─────────────────────────────────┐
│ Service │
│ 10.96.120.50:80 │
│ │ │
│ ┌──────┼──────┐ │
│ ↓ ↓ ↓ │
│ Pod A Pod B Pod C │
│ │ │ │ │
│ 10.244.x 10.244.x 10.244.x │
└─────────────────────────────────┘
The application talks to the Service, never directly to individually changing Pod IPs.
Hands-On Investigation
Try tracing the full chain yourself:
kubectl get pods -o wide
kubectl get svc
kubectl describe svc backend
Look specifically for the Selector: field in the output — e.g., Selector: app=backend. Then check:
kubectl get endpointslices
You're now able to see the entire chain in action:
Service → Selector → EndpointSlice → Pod IPs
How Does the ClusterIP Actually Work?
At this point you might reasonably ask: "Okay, but HOW does 10.96.120.50 actually become 10.244.1.5?"
Great question — and the answer is that something running on every Kubernetes Node has to implement that traffic forwarding and load-balancing behavior. Historically, this has been the job of kube-proxy, using mechanisms like:
- iptables
- IPVS
Modern clusters can also implement this using eBPF. We're deliberately not diving into these internals yet — that's reserved for Level 8. For now, just remember the abstraction:
Service IP → Service dataplane → Backend Pod
Level 3 Checkpoint
1. Why do we need a Service?
→ Because Pod IPs are dynamic and temporary.
2. What does a Service provide?
→ A stable virtual endpoint/IP for accessing a group of Pods.
3. How does a Service find its Pods?
→ Using a selector, typically based on labels.
4. What is a ClusterIP?
→ The stable virtual IP of a Service, normally used for internal cluster access.
5. What is port?
→ The port exposed by the Service itself.
6. What is targetPort?
→ The port on the backend Pod/container that actually receives the traffic.
7. Does a Service create Pods?
→ No. A Deployment manages Pods; a Service provides networking to them.
8. What tracks the current backend endpoints?
→ EndpointSlices.
9. Does Pod-to-Pod communication require a Service?
→ No — Pods can always communicate directly via Pod IPs. A Service is useful when you don't want to depend on individual, changing Pod IPs.
What's Next: Service Discovery and DNS
In Level 4, we'll answer the natural follow-up question this article left open:
How does
backend:80magically turn into a Service IP like10.96.120.50?
We'll cover CoreDNS, DNS records, Service names, namespaces, FQDNs, and Pod DNS — without jumping ahead into the Level 8 dataplane internals.
Conclusion
Services are the answer to one of the most fundamental problems in Kubernetes: Pods are ephemeral, but applications need something stable to talk to. By giving a group of Pods a single, unchanging virtual IP — and continuously tracking the live backend set via EndpointSlices — Kubernetes lets you scale, crash, and replace Pods freely without ever breaking the applications that depend on them.
The mental model to hold onto: Pods are workloads with temporary IPs; Services are stable networking abstractions in front of them. Everything else — ClusterIP, NodePort, LoadBalancer, selectors — is just a variation on that one idea.
Found this helpful? Follow along for Level 4, where we connect Service names to DNS and finally explain how backend resolves into a real IP address. Drop your questions below, and share this with a teammate who still isn't sure why Services and Deployments are two separate objects. Let's keep building this Kubernetes networking roadmap together. 🚀
Top comments (0)