Module 10 — Services
Overview
Services provide stable networking for pods. Since pods are ephemeral (they get new IPs when recreated), Services give a permanent IP and DNS name that routes traffic to the right pods. The CKA exam tests all four Service types, Endpoints, EndpointSlices, and headless Services.
1. Service Fundamentals
1.1 The Problem Services Solve
| Without Services:
Client → Pod IP (10.244.1.5)
Pod dies, new pod gets 10.244.2.8
Client breaks ✗
With Services:
Client → Service IP (10.96.0.10) → Pod IP (10.244.1.5)
Pod dies, new pod gets 10.244.2.8
Service routes to new pod automatically ✓
|
1.2 How Services Work
| ┌──────────┐ ┌──────────────────┐ ┌──────────┐
│ Client │────▶│ Service │────▶│ Pod A │
│ │ │ 10.96.0.10:80 │ ┌─▶│ 10.244.1.5│
│ │ │ │ │ └──────────┘
│ │ │ selector: │──┤
│ │ │ app: web │ │ ┌──────────┐
│ │ │ │ └─▶│ Pod B │
│ │ │ Endpoints: │ │ 10.244.2.8│
│ │ │ 10.244.1.5:8080 │ └──────────┘
│ │ │ 10.244.2.8:8080 │
└──────────┘ └──────────────────┘
|
- Service uses a selector to find matching pods
- Matching pod IPs are stored in Endpoints/EndpointSlices
- kube-proxy programs iptables/ipvs rules on every node to route Service IP → Pod IPs
- Traffic is load-balanced across pods (random or round-robin)
1.3 Service YAML Structure
| apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP # Service type
selector:
app: web # matches pods with this label
ports:
- name: http # optional but recommended
port: 80 # Service port (what clients connect to)
targetPort: 8080 # Pod port (where the container listens)
protocol: TCP # TCP (default), UDP, or SCTP
|
| Field |
Purpose |
spec.type |
ClusterIP, NodePort, LoadBalancer, or ExternalName |
spec.selector |
Label selector to find backend pods |
spec.ports[].port |
Port exposed by the Service |
spec.ports[].targetPort |
Port on the pod (can be a number or named port) |
spec.ports[].nodePort |
(NodePort/LoadBalancer only) Port on every node |
2. Service Types
2.1 ClusterIP (Default)
Exposes the Service on an internal cluster IP. Only reachable from within the cluster.
| ┌─────────────────────────────────────────┐
│ Cluster │
│ │
│ Client Pod ──▶ ClusterIP:80 ──▶ Pods │
│ 10.96.0.10 │
│ │
└─────────────────────────────────────────┘
External ✗ (not reachable from outside)
|
| # Imperative
kubectl expose deployment web --port=80 --target-port=8080 --type=ClusterIP
# Or create the deployment and service together
kubectl create deployment web --image=nginx --replicas=3
kubectl expose deployment web --port=80 --target-port=80
|
| apiVersion: v1
kind: Service
metadata:
name: web-clusterip
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 8080
|
Use cases:
- Internal microservice communication
- Database access within the cluster
- Backend APIs consumed by other pods
2.2 NodePort
Exposes the Service on a static port on every node's IP. Accessible from outside the cluster via <NodeIP>:<NodePort>.
| ┌─────────────────────────────────────────────────┐
│ Cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ :30080 │ │ :30080 │ │ :30080 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ ClusterIP:80 ──▶ Pods │
└─────────────────────────────────────────────────┘
External ──▶ <any-node-ip>:30080
|
| # Imperative
kubectl expose deployment web --port=80 --target-port=8080 --type=NodePort
|
| apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80 # ClusterIP port
targetPort: 8080 # Pod port
nodePort: 30080 # Node port (30000-32767, auto-assigned if omitted)
|
| Aspect |
Detail |
| Port range |
30000–32767 (configurable via API server flag) |
| Auto-assignment |
If nodePort is omitted, Kubernetes picks one |
| Accessibility |
<any-node-ip>:<nodePort> from outside the cluster |
| Also creates |
A ClusterIP (NodePort is a superset of ClusterIP) |
2.3 LoadBalancer
Exposes the Service via a cloud provider's load balancer. Creates a NodePort and ClusterIP automatically.
| ┌──────────────────────────────────────────────────────┐
│ │
│ Internet ──▶ Cloud LB (external IP) ──▶ NodePort │
│ 203.0.113.50:80 :30080 │
│ │ │
│ ClusterIP:80 │
│ │ │
│ Pods │
└──────────────────────────────────────────────────────┘
|
| # Imperative
kubectl expose deployment web --port=80 --target-port=8080 --type=LoadBalancer
|
| apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080
|
| # Check external IP (may take a minute to provision)
kubectl get svc web-lb
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# web-lb LoadBalancer 10.96.0.15 203.0.113.50 80:30080/TCP 2m
# If EXTERNAL-IP shows <pending>, the cloud provider hasn't provisioned yet
# On bare-metal without a LB controller, it stays <pending> forever
|
CKA Tip: On exam environments (usually bare-metal/VM), LoadBalancer Services will show <pending> for EXTERNAL-IP. The NodePort still works.
2.4 ExternalName
Maps a Service to an external DNS name. No proxying — just a CNAME DNS record.
| Pod ──▶ my-db.default.svc.cluster.local
│
└──▶ CNAME: db.example.com
│
└──▶ external database
|
| apiVersion: v1
kind: Service
metadata:
name: my-db
spec:
type: ExternalName
externalName: db.example.com # external DNS name
|
- No selector, no ClusterIP, no Endpoints
- CoreDNS returns a CNAME record
- Useful for referencing external services with an in-cluster DNS name
2.5 Type Comparison
| Type |
ClusterIP |
NodePort |
LoadBalancer |
ExternalName |
| Internal access |
✅ |
✅ |
✅ |
✅ (DNS only) |
| External access |
❌ |
✅ (node IP) |
✅ (LB IP) |
N/A |
| Creates ClusterIP |
Yes |
Yes |
Yes |
No |
| Creates NodePort |
No |
Yes |
Yes |
No |
| Creates LB |
No |
No |
Yes (cloud) |
No |
| Selector |
Yes |
Yes |
Yes |
No |
| Use case |
Internal |
Dev/test |
Production |
External DNS alias |
3. Endpoints and EndpointSlices
3.1 Endpoints
An Endpoints object is automatically created for every Service with a selector. It lists the IP:port of all matching pods.
| # View Endpoints
kubectl get endpoints web-clusterip
# NAME ENDPOINTS AGE
# web-clusterip 10.244.1.5:8080,10.244.2.8:8080,10.244.3.2:8080 5m
# Detailed view
kubectl describe endpoints web-clusterip
|
When a pod becomes ready, its IP is added to Endpoints. When it becomes not-ready or is deleted, it's removed.
3.2 EndpointSlices
EndpointSlices are the modern replacement for Endpoints. They split endpoint data into smaller chunks for better scalability.
| # View EndpointSlices
kubectl get endpointslices
kubectl get endpointslices -l kubernetes.io/service-name=web-clusterip
# Detailed view
kubectl describe endpointslice <name>
|
| Aspect |
Endpoints |
EndpointSlices |
| Introduced |
v1.0 |
v1.17 (GA in v1.21) |
| Max entries |
Unlimited (single object) |
100 per slice (multiple slices) |
| Scalability |
Poor for large services |
Designed for scale |
| Dual-stack |
Limited |
Full IPv4/IPv6 support |
| Used by |
Legacy kube-proxy |
Modern kube-proxy |
CKA Tip: You'll mostly interact with Endpoints for debugging. EndpointSlices work the same way but are split into smaller objects.
3.3 Manual Endpoints (Services Without Selectors)
You can create a Service without a selector and manually define Endpoints — useful for pointing to external services or specific IPs:
| # Service without selector
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ClusterIP
ports:
- port: 3306
targetPort: 3306
---
# Manual Endpoints (same name as the Service)
apiVersion: v1
kind: Endpoints
metadata:
name: external-db # must match Service name
subsets:
- addresses:
- ip: 192.168.1.100 # external DB IP
- ip: 192.168.1.101 # second DB IP
ports:
- port: 3306
|
Now pods can reach the external database via external-db:3306.
3.4 Debugging Endpoints
| # Service exists but no Endpoints — common issue
kubectl get svc my-service
kubectl get endpoints my-service
# NAME ENDPOINTS AGE
# my-service <none> 5m ← no matching pods!
# Troubleshooting checklist:
# 1. Check the Service selector
kubectl describe svc my-service | grep Selector
# 2. Check if pods have matching labels
kubectl get pods --show-labels
# 3. Check if pods are Ready
kubectl get pods
# Pods in CrashLoopBackOff or not passing readiness probes
# won't appear in Endpoints
|
4. Headless Services
4.1 What Is a Headless Service?
A Service with clusterIP: None. Instead of a single virtual IP, DNS returns the individual pod IPs directly.
| Regular Service:
nslookup web-service → 10.96.0.10 (ClusterIP)
Headless Service:
nslookup web-headless → 10.244.1.5
10.244.2.8
10.244.3.2 (all pod IPs)
|
4.2 Creating a Headless Service
| apiVersion: v1
kind: Service
metadata:
name: web-headless
spec:
clusterIP: None # ← this makes it headless
selector:
app: web
ports:
- port: 80
targetPort: 8080
|
| # Imperative (then edit to add clusterIP: None)
kubectl expose deployment web --port=80 --target-port=8080 --cluster-ip=None
|
4.3 DNS Behavior
| # From inside a pod:
# Regular Service — returns ClusterIP
nslookup web-clusterip.default.svc.cluster.local
# Address: 10.96.0.10
# Headless Service — returns all pod IPs
nslookup web-headless.default.svc.cluster.local
# Address: 10.244.1.5
# Address: 10.244.2.8
# Address: 10.244.3.2
|
4.4 Use Cases
| Use case |
Why headless? |
| StatefulSets |
Each pod needs a unique DNS name (pod-0.svc, pod-1.svc) |
| Client-side load balancing |
Application chooses which pod to connect to |
| Service discovery |
Get all pod IPs for custom routing logic |
| Database clusters |
Connect to a specific replica (primary vs read-replica) |
4.5 Headless + StatefulSet DNS
| # StatefulSet with serviceName: mysql-headless
# Pods: mysql-0, mysql-1, mysql-2
|
DNS records created:
| mysql-headless.default.svc.cluster.local → all pod IPs (A records)
mysql-0.mysql-headless.default.svc.cluster.local → 10.244.1.5
mysql-1.mysql-headless.default.svc.cluster.local → 10.244.2.8
mysql-2.mysql-headless.default.svc.cluster.local → 10.244.3.2
|
Each pod gets a stable DNS name that persists across restarts (the IP may change, but the DNS name stays).
5. Service DNS
Every Service gets a DNS record:
| <service-name>.<namespace>.svc.cluster.local
|
| DNS Name |
Resolves to |
web |
ClusterIP (from same namespace) |
web.default |
ClusterIP (cross-namespace) |
web.default.svc |
ClusterIP |
web.default.svc.cluster.local |
ClusterIP (fully qualified) |
5.2 Cross-Namespace Access
| # Pod in namespace "frontend" accessing Service in namespace "backend"
curl http://api-service.backend.svc.cluster.local:80
# Short form (if DNS search domains are configured)
curl http://api-service.backend:80
|
5.3 SRV Records (Port Discovery)
| # SRV records include port information
nslookup -type=SRV _http._tcp.web-service.default.svc.cluster.local
|
6. Multi-Port Services
| apiVersion: v1
kind: Service
metadata:
name: multi-port
spec:
selector:
app: web
ports:
- name: http # name is REQUIRED for multi-port
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
- name: metrics
port: 9090
targetPort: 9090
|
CKA Tip: When a Service has multiple ports, each port MUST have a name.
7. Named Ports
You can reference container ports by name instead of number:
| # In the Pod/Deployment
spec:
containers:
- name: app
ports:
- name: http
containerPort: 8080
- name: metrics
containerPort: 9090
---
# In the Service
spec:
ports:
- port: 80
targetPort: http # references the named port
- port: 9090
targetPort: metrics
|
Benefit: if the container port number changes, you only update the pod spec — the Service still works.
8. Session Affinity
By default, Services distribute traffic randomly. Session affinity routes a client to the same pod:
| spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hours (default)
|
| Value |
Behavior |
None (default) |
Random distribution |
ClientIP |
Same client IP → same pod |
9. Useful Commands
| # --- Create ---
kubectl expose deployment <name> --port=80 --target-port=8080 --type=ClusterIP
kubectl expose deployment <name> --port=80 --type=NodePort
kubectl create service clusterip my-svc --tcp=80:8080
kubectl create service nodeport my-svc --tcp=80:8080 --node-port=30080
# --- Inspect ---
kubectl get svc
kubectl get svc -o wide # shows selector
kubectl describe svc <name> # shows Endpoints
kubectl get endpoints <name>
kubectl get endpointslices -l kubernetes.io/service-name=<name>
# --- Debug connectivity ---
# From a debug pod
kubectl run debug --image=busybox -it --rm --restart=Never -- sh
nslookup <service-name>
wget -qO- http://<service-name>:<port>
nc -zv <service-name> <port>
# Check iptables rules (on a node)
iptables -t nat -L KUBE-SERVICES -n | grep <service-name>
# Check kube-proxy logs
kubectl logs -n kube-system -l k8s-app=kube-proxy
|
10. Practice Exercises
Exercise 1 — ClusterIP Service
| # 1. Create a deployment
kubectl create deployment web --image=nginx --replicas=3
# 2. Expose as ClusterIP
kubectl expose deployment web --port=80 --target-port=80
# 3. Verify
kubectl get svc web
kubectl get endpoints web
# 4. Test from a debug pod
kubectl run debug --image=busybox -it --rm --restart=Never -- wget -qO- http://web
# 5. Clean up
kubectl delete deployment web
kubectl delete svc web
|
Exercise 2 — NodePort Service
| # 1. Create a deployment
kubectl create deployment nodeport-test --image=nginx --replicas=2
# 2. Expose as NodePort
kubectl expose deployment nodeport-test --port=80 --target-port=80 --type=NodePort
# 3. Find the assigned NodePort
kubectl get svc nodeport-test
# Note the port in the 30000-32767 range
# 4. Access from any node IP
curl http://<node-ip>:<node-port>
# 5. Clean up
kubectl delete deployment nodeport-test
kubectl delete svc nodeport-test
|
Exercise 3 — Headless Service
| # 1. Create a deployment
kubectl create deployment headless-test --image=nginx --replicas=3
# 2. Create a headless Service
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: headless-test
spec:
clusterIP: None
selector:
app: headless-test
ports:
- port: 80
targetPort: 80
EOF
# 3. Compare DNS resolution
kubectl run debug --image=busybox -it --rm --restart=Never -- sh
# Inside the pod:
nslookup headless-test.default.svc.cluster.local
# Should return multiple pod IPs, not a single ClusterIP
# 4. Clean up
kubectl delete deployment headless-test
kubectl delete svc headless-test
|
Exercise 4 — Service Without Selector (Manual Endpoints)
| # 1. Create a Service without selector
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: external-svc
spec:
ports:
- port: 80
targetPort: 80
EOF
# 2. Check Endpoints — should be empty
kubectl get endpoints external-svc
# 3. Create manual Endpoints
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Endpoints
metadata:
name: external-svc
subsets:
- addresses:
- ip: 93.184.216.34
ports:
- port: 80
EOF
# 4. Verify
kubectl get endpoints external-svc
kubectl describe svc external-svc
# 5. Clean up
kubectl delete svc external-svc
kubectl delete endpoints external-svc
|
Exercise 5 — Troubleshoot a Broken Service
| # 1. Create a deployment with label app=backend
kubectl create deployment backend --image=nginx --replicas=2
# 2. Create a Service with WRONG selector
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: backend-svc
spec:
selector:
app: frontend # BUG: should be "backend"
ports:
- port: 80
targetPort: 80
EOF
# 3. Check Endpoints — empty!
kubectl get endpoints backend-svc
# backend-svc <none>
# 4. Debug
kubectl describe svc backend-svc | grep Selector
kubectl get pods --show-labels | grep backend
# 5. Fix the selector
kubectl patch svc backend-svc -p '{"spec":{"selector":{"app":"backend"}}}'
# 6. Verify Endpoints are populated
kubectl get endpoints backend-svc
# 7. Clean up
kubectl delete deployment backend
kubectl delete svc backend-svc
|
11. Key Takeaways for the CKA Exam
| Point |
Detail |
| ClusterIP is the default |
Internal only — most common for inter-service communication |
| NodePort range: 30000–32767 |
Auto-assigned if not specified |
| LoadBalancer = NodePort + cloud LB |
Shows <pending> on bare-metal without a LB controller |
| ExternalName = CNAME only |
No proxy, no selector, no Endpoints |
kubectl expose is the fastest way |
Creates a Service from a Deployment/Pod |
| Endpoints must match Service name |
For manual Endpoints (no selector) |
| Empty Endpoints = selector mismatch |
Check labels on pods vs selector on Service |
Headless: clusterIP: None |
DNS returns pod IPs directly — required for StatefulSets |
| Multi-port Services need port names |
Each port must have a unique name |
Cross-namespace: svc.namespace |
my-svc.other-ns.svc.cluster.local |
targetPort can be a named port |
References containerPort name in the pod spec |
Previous: 09-configmaps-secrets.md — ConfigMaps & Secrets
Next: 11-networking-model.md — Networking Model & DNS