Skip to content

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

1
2
3
4
5
6
7
8
9
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 │
└──────────┘     └──────────────────┘
  1. Service uses a selector to find matching pods
  2. Matching pod IPs are stored in Endpoints/EndpointSlices
  3. kube-proxy programs iptables/ipvs rules on every node to route Service IP → Pod IPs
  4. 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.

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────┐
│              Cluster                     │
│                                         │
│  Client Pod ──▶ ClusterIP:80 ──▶ Pods  │
│                 10.96.0.10              │
│                                         │
└─────────────────────────────────────────┘
  External ✗ (not reachable from outside)
1
2
3
4
5
6
# 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.

1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────────┐
│                                                      │
│  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
1
2
3
4
5
6
7
# 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.

1
2
3
4
5
Pod ──▶ my-db.default.svc.cluster.local
              └──▶ CNAME: db.example.com
                          └──▶ external database
1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
# 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.

1
2
3
4
5
6
# 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.

1
2
3
4
5
6
7
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:

1
2
3
4
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

5.1 DNS Format

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

1
2
3
4
5
# 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:

1
2
3
4
5
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