Services and Networking: Stable Access to Dynamic Pods
Services solve the Pod ephemerality problem.
- Status
- stable
- Trust
- unverified
- Approved
- ag-archive/0.1.0 ·
Your Deployment from Lesson 4 is running Pods. But there's a problem: Pods are ephemeral. When a Pod crashes and is replaced, it gets a new IP address. Your application clients can't track these constant changes.
Enter Services. A Service is a stable, virtual IP address that routes to a dynamic set of Pods. It's like a phone number that rings to whoever happens to be on duty today—not tied to one person, but always available.
In this lesson, you'll understand why Services are necessary, learn the three main Service types, and master the mechanism that makes it all work: label selectors.
The Networking Problem: Pods Are Ephemeral
In Docker (Chapter 79), a container had a stable network identity. You'd run a container, learn its IP, and talk to it.
Kubernetes is different. Pods are designed to be replaced:
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
containers:
- name: nginx
image: nginx:alpineWhen this Pod crashes and Kubernetes restarts it, a new Pod takes its place. But the new Pod gets a new IP address. The old IP becomes invalid.
The Direct IP Problem
Imagine your frontend application tries to talk to your backend:
# In a frontend Pod
response = requests.get("http://10.0.2.45:5000/data")If the backend Pod gets replaced (it will), 10.0.2.45 no longer exists. The request fails.
This is unacceptable in production. You need a stable address that routes to the current set of healthy backend Pods, regardless of which specific Pods are running.
Enter Services.
Services: Stable Virtual IP Addresses
A Service is a Kubernetes abstraction that provides:
- A stable virtual IP address (ClusterIP) that doesn't change
- Load balancing across Pods matching the Service's label selector
- Service discovery through Kubernetes DNS
Think of a Service like a phone number. The number stays the same, but the person who answers might change. Call the same number and get connected to whoever is currently on duty.
How Services Work
A Service uses label selectors to determine which Pods receive traffic. Here's the mechanism:
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
selector:
app: backend # <-- Label selector: "find Pods with label app=backend"
ports:
- port: 80 # <-- Service listens on port 80
targetPort: 5000 # <-- Forward to port 5000 in the Pod
type: ClusterIP # <-- Service type (discussed below)When you apply this Service:
- Kubernetes creates a virtual IP address (e.g.,
10.96.0.1) - It identifies all Pods matching
app: backend - It load-balances traffic to those Pods' port 5000
- If Pods are added/removed, the Service updates automatically
Your frontend Pod now talks to:
response = requests.get("http://backend-service:80/data")Even when backend Pods crash and restart, the Service name and IP stay constant. Kubernetes automatically updates the endpoints.
The Three Service Types
Kubernetes offers three main Service types. Choose based on your access pattern.
1. ClusterIP: Internal Communication (Default)
Use when: Pods need to talk to other Pods within the cluster.
apiVersion: v1
kind: Service
metadata:
name: database-service
spec:
selector:
app: database
ports:
- port: 5432
targetPort: 5432
type: ClusterIP # Default if omittedCharacteristics:
- Virtual IP only accessible from within the cluster
- No external access possible
- Lightweight and efficient
- Most common type (used internally between Pods)
Example traffic flow:
Frontend Pod → (requests) → ClusterIP Service (10.96.0.5:5432)
→ (routes to) → Database Pod2. NodePort: External Access via Host Port
Use when: You need external clients to access your application (development/testing).
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api
ports:
- port: 80 # Service port (internal)
targetPort: 8000 # Pod port
nodePort: 30007 # Host port (must be 30000-32767)
type: NodePortCharacteristics:
- Allocates a port on every cluster node (30000-32767 range)
- External clients access
<node-ip>:<nodePort> - Useful for development and testing
- Not recommended for production (traffic through nodes)
Example traffic flow:
External Client → (connects to) → Node IP:30007
→ (routes to) → Service (10.96.0.1:80)
→ (load balances to) → API Pods3. LoadBalancer: Production External Access
Use when: You need production-grade external access with load balancing.
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 3000
type: LoadBalancerCharacteristics:
- Provisions a cloud load balancer (AWS ELB, Azure LB, GCP LB)
- Gives your Service a public IP address
- Load balancer distributes traffic across cluster nodes
- Recommended for production (handles high traffic, automatic scaling)
Example traffic flow:
External Client → (connects to) → Cloud LB Public IP:80
→ (routes to) → Service (10.96.0.2:80)
→ (load balances to) → Frontend PodsDecision Framework: Choosing Service Type
| Requirement | Type | Why |
|---|---|---|
| Pod-to-Pod communication | ClusterIP | No external access needed, most efficient |
| Manual external testing | NodePort | Quick, no cloud LB needed |
| Production external access | LoadBalancer | Cloud LB handles scalability, reliability |
| Multi-environment consistency | ClusterIP + Ingress | (Ingress covered in later chapter) |
Understanding Label Selectors
Label selectors are the mechanism that connects Services to Pods. They're critical to understand because misconfigured selectors cause Services to have zero endpoints.
How Label Selectors Work
When you define a Service selector, Kubernetes continuously asks: "Which Pods match these labels?"
# Service definition
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web # <-- Kubernetes finds Pods with app: web
version: v1 # <-- AND version: v1
ports:
- port: 80
targetPort: 8080This Service routes traffic to Pods labeled both app: web and version: v1.
Example: Label Matching
Pod 1: Matches the selector
apiVersion: v1
kind: Pod
metadata:
name: web-pod-1
labels:
app: web # <-- Matches
version: v1 # <-- Matches
env: prod # <-- Extra label (OK)
spec:
containers:
- name: nginx
image: nginx:alpinePod 2: Does NOT match
apiVersion: v1
kind: Pod
metadata:
name: api-pod-1
labels:
app: api # <-- Does NOT match (wrong value)
version: v1 # <-- Matches but doesn't satisfy both
spec:
containers:
- name: flask
image: flask:latestPod 3: Does NOT match
apiVersion: v1
kind: Pod
metadata:
name: web-pod-2
labels:
app: web # <-- Matches
version: v2 # <-- Does NOT match (wrong version)
spec:
containers:
- name: nginx
image: nginx:alpineThe Service routes to Pod 1 only. Pods 2 and 3 are ignored.
Hands-On: Creating and Exposing Services
You'll create a Deployment, then expose it via different Service types. Start with your Docker Desktop Kubernetes cluster running.
Step 1: Create a Deployment with Clear Labels
Create a file called web-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-deployment
spec:
replicas: 3
selector:
matchLabels:
app: web # <-- Key label
template:
metadata:
labels:
app: web # <-- Pod gets this label
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80Apply it:
kubectl apply -f web-deployment.yamlOutput:
deployment.apps/web-deployment createdVerify Pods are running with labels displayed:
kubectl get pods --show-labelsOutput:
NAME READY STATUS RESTARTS AGE LABELS
web-deployment-5d5f4c7f5b-abc12 1/1 Running 0 10s app=web,pod-template-hash=5d5f4c7f5b
web-deployment-5d5f4c7f5b-def45 1/1 Running 0 10s app=web,pod-template-hash=5d5f4c7f5b
web-deployment-5d5f4c7f5b-ghi78 1/1 Running 0 10s app=web,pod-template-hash=5d5f4c7f5bNotice each Pod has the label app=web (the key label the Service will use). Get their IP addresses:
kubectl get pods -o wideOutput:
NAME READY STATUS RESTARTS AGE IP NODE
web-deployment-5d5f4c7f5b-abc12 1/1 Running 0 10s 10.244.0.3 docker-desktop
web-deployment-5d5f4c7f5b-def45 1/1 Running 0 10s 10.244.0.4 docker-desktop
web-deployment-5d5f4c7f5b-ghi78 1/1 Running 0 10s 10.244.0.5 docker-desktopEach Pod has a unique IP: 10.244.0.3, 10.244.0.4, 10.244.0.5. If a Pod crashes, Kubernetes replaces it with a new Pod at a new IP. This is why Services matter—they provide stable routing regardless of which Pod IPs are currently active.
Step 2: Expose via ClusterIP Service
Create web-service-clusterip.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web # <-- Targets Pods with app: web
ports:
- port: 80
targetPort: 80
type: ClusterIPApply it:
kubectl apply -f web-service-clusterip.yamlOutput:
service/web-service createdInspect the Service:
kubectl get servicesOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
web-service ClusterIP 10.96.0.234 <none> 80/TCPThe Service has a stable virtual IP: 10.96.0.234. This IP never changes, even if Pods are replaced.
Step 3: Access the Service Internally
Launch a temporary Pod inside the cluster to test internal access:
kubectl run -it test-pod --image=alpine --rm -- shFrom inside the Pod, curl the Service:
apk add curl
curl http://web-serviceOutput:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
</head>
...Success! The Service routed your request to one of the backend Pods. Try it again:
curl http://web-serviceSame output. Kubernetes load-balanced across Pods transparently. You didn't need to know which specific Pod answered—the Service handled it.
Exit the test Pod:
exitStep 4: Convert to NodePort for External Access
Update web-service-clusterip.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30007 # <-- Add this (external port)
type: NodePort # <-- Change thisApply the update:
kubectl apply -f web-service-clusterip.yamlOutput:
service/web-service configuredVerify the change:
kubectl get servicesOutput:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
web-service NodePort 10.96.0.234 <none> 80:30007/TCPThe Service now listens on node port 30007. With Docker Desktop, access the Service via localhost:
curl http://localhost:30007Output:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
</head>
...Success! External traffic now reaches your Deployment through the NodePort Service.
Kubernetes DNS: Service Discovery
Your Pods don't need to know the Service IP address. Kubernetes provides automatic DNS discovery.
Every Service gets a DNS name in the format:
<service-name>.<namespace>.svc.cluster.localFor the web-service in the default namespace:
web-service.default.svc.cluster.localWithin the same namespace, you can shorten it to:
web-serviceDNS in Action
Launch a test Pod and resolve the DNS name:
kubectl run -it test-pod --image=alpine --rm -- shFrom inside the Pod, install curl and bind-tools (for DNS utilities), then test DNS resolution:
apk add curl bind-tools
nslookup web-serviceOutput:
Name: web-service
Address 1: 10.96.0.234Kubernetes DNS returned the Service's virtual IP. Your application can connect without hardcoding IPs. Test it:
curl -I http://web-serviceOutput:
HTTP/1.1 200 OK
Server: nginx/1.26.2
Date: Fri, 22 Dec 2023 10:15:32 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-aliveThe request succeeded! Kubernetes DNS resolved web-service to the virtual IP, and the Service routed to a backend Pod.
You can also use the full DNS name from different namespaces:
curl -I http://web-service.default.svc.cluster.localOutput:
HTTP/1.1 200 OK
Server: nginx/1.26.2
Date: Fri, 22 Dec 2023 10:15:35 GMT
Content-Type: text/html
Content-Length: 612
Connection: keep-aliveBoth work. Kubernetes DNS resolves either form to the virtual IP. Exit the test Pod:
exitDebugging: Service Selector Mismatch
The most common Service problem: a Service has no endpoints. This happens when the label selector doesn't match any Pods.
Scenario: Misconfigured Selector
Create a Deployment with app: backend:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: flask
image: flask:latest
ports:
- containerPort: 5000Apply it:
kubectl apply -f backend-deployment.yamlOutput:
deployment.apps/backend-deployment createdNow create a Service with the wrong selector:
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
selector:
app: api # <-- WRONG: Looking for app: api, but Pods have app: backend
ports:
- port: 80
targetPort: 5000
type: ClusterIPApply it:
kubectl apply -f backend-service-wrong.yamlOutput:
service/backend-service createdCheck the Service endpoints:
kubectl get endpoints backend-serviceOutput:
NAME ENDPOINTS AGE
backend-service <none> 10sNo endpoints! The Service exists but routes to nothing. Any traffic sent to this Service fails because there's no Pod to handle it.
Fixing the Selector
Update the Service with the correct selector:
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
selector:
app: backend # <-- FIXED: Now matches the Pod labels
ports:
- port: 80
targetPort: 5000
type: ClusterIPApply the fix:
kubectl apply -f backend-service-wrong.yamlOutput:
service/backend-service configuredCheck endpoints again:
kubectl get endpoints backend-serviceOutput:
NAME ENDPOINTS AGE
backend-service 10.244.0.6:5000,10.244.0.7:5000 5sNow the Service has 2 endpoints (the 2 backend Pods). Traffic will route correctly.
Debugging Process
When a Service has no endpoints, follow this systematic check:
- List the Pods and their labels:
kubectl get pods --show-labelsOutput:
NAME READY LABELS
backend-deployment-abcd1234-xyz 1/1 app=backend,pod-template-hash=abcd1234
backend-deployment-abcd1234-uvw 1/1 app=backend,pod-template-hash=abcd1234- Check the Service selector:
kubectl describe service backend-serviceOutput:
Name: backend-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app: api # <-- Wrong! Should be app: backend
Type: ClusterIP
IP: 10.96.0.1
Port: <unset> 80/TCP
TargetPort: 5000/TCP
Endpoints: <none>
Session Affinity: None-
Compare: The Pods have
app=backendbut the Service selector isapp=api. That's the mismatch. Fix the Service selector to match the Pod labels. -
Verify the fix:
kubectl describe service backend-serviceOutput:
Name: backend-service
Namespace: default
Selector: app: backend # <-- Fixed!
Type: ClusterIP
IP: 10.96.0.1
Port: <unset> 80/TCP
TargetPort: 5000/TCP
Endpoints: 10.244.0.6:5000,10.244.0.7:5000 # <-- Endpoints now populated!
Session Affinity: NoneNow the Service has endpoints. Traffic will route correctly to the backend Pods.
Service Networking Summary
| Concept | Purpose | Example |
|---|---|---|
| Service | Stable virtual IP routing to dynamic Pods | web-service (IP: 10.96.0.234) |
| Label Selector | Mechanism to match Pods | app: web, version: v1 |
| ClusterIP | Internal Pod-to-Pod communication | Backend database accessed by frontend |
| NodePort | External access via host port | Testing tool accessing API on port 30007 |
| LoadBalancer | Production external access via cloud LB | Public users accessing frontend |
| DNS | Service discovery by name | web-service.default.svc.cluster.local |
Try With AI
Challenge: You have a FastAPI agent running in Kubernetes (from Chapter 79 containerization). Expose it so:
- Other Pods in the cluster can call its API
- External clients can access it for testing
You'll describe the requirements, and you and AI will iterate on the Service configuration.
Setup:
- Your agent Deployment is named
agent-deploymentwith labelapp: agent - It listens on port 8000
- You'll deploy to your Docker Desktop Kubernetes cluster
Part 1: Internal Access Requirement
Tell AI: "I need a Service that lets other Pods access my agent at port 8000. The agent Deployment has label app: agent."
Record AI's Service specification for internal access.
Part 2: Evaluate Internal Design
Review AI's response:
- Does the Service selector match the Deployment labels?
- Is the port mapping correct (80 → 8000)?
- Is the type appropriate for internal access?
Ask yourself: What would you change to make this better?
Part 3: External Access Requirement
Tell AI: "Now I also need external clients to test the agent. Should I convert to NodePort or LoadBalancer? I'm testing locally with Docker Desktop Kubernetes."
Note AI's reasoning about the choice.
Part 4: Design Validation
Ask AI: "Show me the complete Service YAML for external testing access on Docker Desktop Kubernetes."
Apply the YAML to your cluster:
kubectl apply -f agent-service.yamlTest external access:
curl http://localhost:<nodeport>/docs # Your FastAPI agent's Swagger docsPart 5: Reflection
Compare your initial understanding to what emerged:
- Did you initially consider both internal and external access?
- What would you do differently when deploying to a cloud cluster instead of Docker Desktop?
- When would you choose LoadBalancer over NodePort?
These questions activate your reasoning for future Service design decisions in production environments.
Reflect on Your Skill
You built a kubernetes-deployment skill in Lesson 0. Test and improve it based on what you learned.
Test Your Skill
Using my kubernetes-deployment skill, create a Service that exposes a Deployment.
Does my skill generate ClusterIP, NodePort, or LoadBalancer Service types with proper label selectors?Identify Gaps
Ask yourself:
- Did my skill include the three Service types (ClusterIP, NodePort, LoadBalancer) and when to use each?
- Did it explain label selectors as the mechanism connecting Services to Pods?
- Did it cover Kubernetes DNS (service.namespace.svc.cluster.local)?
- Did it include debugging steps for Services with no endpoints?
Improve Your Skill
If you found gaps:
My kubernetes-deployment skill is missing Service networking patterns and DNS discovery.
Update it to include Service type selection, label selector configuration, DNS naming conventions, and endpoint troubleshooting.Core Concept
Services solve the Pod ephemerality problem. A Service is a stable virtual IP and DNS name that routes traffic to a dynamic set of Pods. When Pods crash and restart with new IPs, the Service IP and DNS name never change. This decouples clients from Pod instability—they connect to service-name, not pod-ip, and Kubernetes handles the routing underneath.
Key Mental Models
- Service as load balancer + service discovery—Service provides a stable IP (or DNS name) and load-balances traffic across matching Pods. Pods change, Service stays constant
- Label selectors are the connection mechanism—Service uses selectors to find Pods. "Find all Pods with label app=backend" is how the Service knows which Pods to route to. Selector mismatch = zero endpoints = broken service
- ClusterIP (internal) vs NodePort (external) vs LoadBalancer (cloud)—ClusterIP routes within cluster (Pod-to-Pod). NodePort opens a port on each node (development/testing). LoadBalancer provisions a cloud LB (production)
- DNS resolution is automatic—Services get DNS names like service-name.namespace.svc.cluster.local. CoreDNS resolves them. Within-namespace, short names work; across namespaces, use FQDN
Critical Patterns
- Endpoints tell the truth—
kubectl get endpoints service-nameshows actual Pod IPs. No endpoints? Selector doesn't match Pods. This is 90% of Service connectivity issues - Service discovers Pods continuously—Service controller runs constantly. Pods added/removed? Service endpoints update immediately. No manual refreshing
- Port mapping isolates layer 4 from layer 7—Service port (80) routes to Pod port (8080). Clients hit port 80, Pods listen on 8080. Useful for port standardization
Common Mistakes
- Hardcoding Pod IPs (LOST when Pod restarts—use Service names always)
- Service selector doesn't match Pod labels (endpoints show
<none>, traffic fails) - Confusing ClusterIP (virtual, internal) with Pod IP (real, ephemeral)—Services are virtual abstractions over actual Pod IPs
- Using LoadBalancer for everything (expensive in cloud; Ingress is preferred for multiple services)
- Testing with port-forward instead of Service DNS (won't catch networking issues that occur with real Services)
Connections
- Builds on: Pods (Lesson 3) and Deployments (Lesson 4)—Services route to Pods managed by Deployments
- Leads to: Ingress (Lesson 9) for Layer 7 HTTP routing, namespaces (Lesson 8) with cross-namespace discovery, service discovery deep dive (Lesson 10)