DEV Community

Cover image for Kubernetes Networking [Level-3: Services]
ADITYA RAJ
ADITYA RAJ

Posted on

Kubernetes Networking [Level-3: Services]

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

  1. The Problem: Pod IPs Aren't Reliable
  2. The Solution: Service
  3. The Core Mental Model
  4. How a Service Finds Its Pods: Labels and Selectors
  5. A Minimal Service Example
  6. port vs targetPort
  7. What a Service Actually Gives You: ClusterIP
  8. Visualizing Service Selection
  9. A Service Does Not Create Pods
  10. Hands-On: Deploying Backend Pods
  11. Hands-On: Creating the Service
  12. Where Does the Traffic Actually Go?
  13. A Service Isn't a Normal Server
  14. EndpointSlices: Tracking Live Backends
  15. What Happens When a Pod Dies
  16. Kubernetes Service Types
  17. Service vs Pod vs Deployment
  18. Services Don't Mean DNS — Yet
  19. The Complete Picture So Far
  20. Hands-On Investigation
  21. How Does the ClusterIP Actually Work?
  22. Level 3 Checkpoint
  23. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then your Service can declare:

selector:
  app: backend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Don't worry about every field yet — focus on two things:

selector:
  app: backend
Enter fullscreen mode Exit fullscreen mode

and:

port: 80
targetPort: 8080
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Frontend Pod
     |
     | 10.96.0.10:80
     ↓
  Service
     |
     ↓
Backend Pod
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Any Pod that doesn't match the selector is simply excluded:

                 Service
                    |
        ┌───────────┼───────────┐
        ↓           ↓           ↓
     backend      backend     backend
        ✓           ✓           ✓

     frontend
        ✗
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply it:

kubectl apply -f backend.yaml
Enter fullscreen mode Exit fullscreen mode

Check the results:

kubectl get pods -o wide
Enter fullscreen mode Exit fullscreen mode
NAME                       IP
backend-xxxxx-aaa          10.244.1.5
backend-xxxxx-bbb          10.244.2.7
backend-xxxxx-ccc          10.244.2.9
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply it:

kubectl apply -f backend-service.yaml
Enter fullscreen mode Exit fullscreen mode

Check it:

kubectl get svc
Enter fullscreen mode Exit fullscreen mode
NAME      TYPE        CLUSTER-IP     PORT(S)
backend   ClusterIP   10.96.120.50   80/TCP
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Frontend
   |
   | 10.96.120.50:80
   ↓
Service
   |
   ├────→ 10.244.1.5:80
   ├────→ 10.244.2.7:80
   └────→ 10.244.2.9:80
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check them with:

kubectl get endpointslices
Enter fullscreen mode Exit fullscreen mode
kubectl describe endpointslice <name>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A replacement Pod (Pod D) may appear shortly after:

Service
 ├── A
 ├── C
 └── D
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Pod A → Service → Pod B
Enter fullscreen mode Exit fullscreen mode

NodePort

Exposes the Service on a fixed port across every Node in the cluster:

type: NodePort
Enter fullscreen mode Exit fullscreen mode
Client
   |
   ↓
Node IP :30080
   |
   ↓
Service
   |
   ↓
Pod
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Internet
    |
    ↓
Public IP
    |
    ↓
Load Balancer
    |
    ↓
Service
    |
    ├──→ Pod
    ├──→ Pod
    └──→ Pod
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Kubernetes Service name → external.example.com
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In short: Pod = workload, Service = networking abstraction.

Service vs Deployment:

Deployment → manages the desired number of Pods
Service    → provides stable access to those Pods
Enter fullscreen mode Exit fullscreen mode
Deployment
    |
    ├── Pod A
    ├── Pod B
    └── Pod C
          ↑
          |
       Service
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In Level 4, we'll add the missing piece:

Service name → DNS → Service IP
Enter fullscreen mode Exit fullscreen mode

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    │
        └─────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Look specifically for the Selector: field in the output — e.g., Selector: app=backend. Then check:

kubectl get endpointslices
Enter fullscreen mode Exit fullscreen mode

You're now able to see the entire chain in action:

Service → Selector → EndpointSlice → Pod IPs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:80 magically turn into a Service IP like 10.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)