The Problem: Pods Alone Aren't Reachable
By this point in the course, the full workload chain is running: a Deployment manages a ReplicaSet, the ReplicaSet keeps pods alive, and the pods run containers. The application is up — but can users actually reach it?
Check what the pods look like from a networking point of view:
kubectl get pods -o wideNAME IP
nginx-abc 10.244.1.15
nginx-def 10.244.1.16
nginx-xyz 10.244.1.17
Every pod has its own cluster-internal IP. Nothing stops you from connecting to http://10.244.1.15 from inside the cluster — but building anything on top of pod IPs falls apart quickly, for four reasons.
1. Pod IPs are dynamic. Pods are ephemeral — their IPs change on restart. Delete a pod and watch what happens:
kubectl delete pod <pod-name>
kubectl get pods -o wideThe ReplicaSet immediately creates a replacement, but the new pod gets a brand-new IP. A pod that was 10.244.1.15 today might be replaced by one at 10.244.1.22 tomorrow. Any client that hard-coded the old address is now broken.
2. Multiple pods, no single address. A Deployment with three replicas gives you three pods and three IPs. Which one should a client connect to? There is no obvious answer, and clients shouldn't have to pick.
3. No load balancing. If 10,000 users all connect to Pod-1 while Pod-2 and Pod-3 sit idle, running replicas gains you nothing. Traffic needs to be distributed across all healthy pods.
4. No service discovery. In a typical app, the frontend talks to the backend, and the backend talks to the database. Each of those tiers is a set of pods with its own churning IPs. Every restart would ripple address changes through the whole stack. The tiers need a stable way to find each other.
Kubernetes solves all four problems with one object: the Service.
What Is a Service?
A Service is an abstraction that exposes a set of pods as a single network service — a permanent entry point with a stable IP and stable DNS name that load-balances traffic across the pods behind it.
Without a Service, pod ephemerality is a real problem. A Deployment's pods can crash, get rescheduled, or be replaced during a rollout, and each replacement pod gets a brand-new IP — so nothing downstream can hard-code a pod IP and expect it to keep working. There's also no single address to hand out when a Deployment runs multiple replicas: if nginx-deployment has three pods, which one should a client connect to, and what happens when 10,000 clients all try to connect to the same one? A Service solves both problems at once — it watches for pods matching a label selector and gives them one stable IP/DNS name, then load-balances traffic across whichever of those pods are currently healthy. Clients only ever need to know the Service's name; which pods sit behind it, and how many, is free to change without breaking anything.
Browser
│
▼
Service
┌─────┼─────┐
▼ ▼ ▼
Pod-1 Pod-2 Pod-3
Clients never connect directly to pods — they connect only to the Service. In summary, a Service provides:
| Capability | What it means |
|---|---|
| Stable IP | The Service's cluster IP never changes, no matter how pods churn |
| Stable DNS | Every Service gets a DNS name resolvable inside the cluster |
| Load balancing | Traffic is distributed across all healthy matching pods |
| Service discovery | Other workloads find the app by name instead of IP |
| Single entry point | One address in front of any number of replicas |
One important clarification: a Service does not create pods. It only exposes pods that already exist (usually managed by a Deployment) and forwards traffic to them.
How a Service Finds Pods
A Service selects its backing pods using the same labels and selectors mechanism used everywhere else in Kubernetes. The pods carry labels:
labels:
app: nginxThe Service declares a selector:
selector:
app: nginxEvery pod with the label app=nginx is automatically picked up as a backend. Kubernetes tracks the current set of matching pod IPs in an Endpoints object with the same name as the Service — as pods come and go, the endpoints list updates itself, and the Service always routes to whatever is currently alive.
Service YAML
A complete ClusterIP Service manifest:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIPEvery field explained:
| Field | Value | Meaning |
|---|---|---|
apiVersion | v1 | Services belong to the core API group |
kind | Service | The object type to create |
metadata.name | nginx-service | Name of the Service — also becomes its DNS name |
spec.selector | app: nginx | Which pods receive traffic: every pod labeled app=nginx |
spec.ports.port | 80 | Port the Service itself exposes — clients connect here |
spec.ports.targetPort | 80 | Container port the traffic is forwarded to |
spec.type | ClusterIP | How the Service is exposed (ClusterIP is the default) |
The distinction between port and targetPort matters most when they differ. Here the Service listens on 80 but the container serves on 3000:
# service.yml
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app # matches pods with this label
ports:
- port: 80 # service port
targetPort: 3000 # container port
type: ClusterIPA request to my-app:80 is forwarded to port 3000 inside whichever pod is chosen:
Client ──▶ Service :80 ──▶ Pod :3000
Creating and Verifying a Service
Apply the manifest just like any other object:
kubectl apply -f service.yamlservice/nginx-service created
List Services and note the cluster IP — unlike pod IPs, it stays stable for the lifetime of the Service:
kubectl get svcNAME TYPE CLUSTER-IP
nginx-service ClusterIP 10.96.20.15
Inspect the details, then check which pods are actually attached:
kubectl describe svc nginx-service
kubectl get endpoints nginx-serviceThe endpoints list shows the pod IPs currently backing the Service. To see the self-healing behavior in action, delete one pod and check again:
kubectl delete pod <pod-name>
kubectl get endpoints nginx-serviceThe dead pod's IP disappears from the list, the replacement's IP appears once it's ready, and in the meantime the Service routes traffic to the remaining pods. No client ever notices.
DNS-Based Service Discovery
Nothing inside the cluster should memorize 10.96.20.15 — Services are reached by name. Every Service automatically gets a DNS entry of the form:
<service-name>.<namespace>.svc.cluster.local
Within the same namespace, the short name alone resolves:
# From a pod in the same namespace
curl http://my-app/health
# From a pod in a different namespace
curl http://my-app.production.svc.cluster.local/health
# Resolve DNS
kubectl exec -it debug-pod -- nslookup my-appThis is what makes service discovery work: the frontend connects to http://backend, the backend connects to postgres:5432, and none of them care which pods currently sit behind those names or what their IPs are.
Load Balancing Behavior
With three pods behind a Service, incoming connections are spread across them — user 1 might land on Pod-1, user 2 on Pod-2, user 3 on Pod-3. No single pod gets overloaded, and adding replicas to the Deployment automatically adds capacity behind the same stable address, with no client-side changes.
Service Types (Preview)
Everything above used type: ClusterIP, the default, which makes the Service reachable only from inside the cluster. Kubernetes offers four Service types in total — ClusterIP, NodePort, LoadBalancer, and ExternalName — which differ in where the Service is reachable from (inside the cluster, on a node port, behind a cloud load balancer, or as an alias to an external DNS name). The next lesson covers each type in depth, including when to use which.
Useful Networking Commands
# List services
kubectl get services
kubectl get svc -A # all namespaces
# Test connectivity from inside cluster
kubectl run debug --image=curlimages/curl -it --rm -- sh
# Inside: curl http://my-app.default.svc.cluster.local/health
# Port forward a service locally
kubectl port-forward svc/my-app 3000:80
# Check endpoints (pods behind a service)
kubectl get endpoints my-appWhen a service doesn't route traffic correctly, check kubectl get endpoints <service>. If the list is empty, the service selector doesn't match any pod labels. Compare kubectl get pods --show-labels with the service's selector field.