Services: Stable Networking for Unstable Pods
Pods are disposable. Their names and IP addresses can change. A Kubernetes Service gives those changing Pods a stable network identity so other workloads can reach them reliably.
What this lesson covers
In the last lesson, you learned that Deployments and ReplicaSets keep the right number of Pods running. But that creates a networking problem: Pods can be deleted, replaced, and recreated with different IP addresses.
By the end of this lesson, you should understand:
- Why Pods need stable network access
- What a Kubernetes Service is
- How Services use labels and selectors
- What endpoints are
- The difference between ClusterIP and NodePort
- How to create a Service imperatively
- How to create a Service from YAML
- How to test a Service with port-forwarding
- How to troubleshoot Services with no endpoints
The short version
A Service is a stable network abstraction in front of one or more Pods.
Instead of connecting directly to a Pod IP, clients connect to the Service. The Service then routes traffic to matching Pods.
Client
connects to Service
Service selects matching Pods
Pods run the application containers
This matters because Pods are temporary. Services give applications a stable way to communicate even as Pods come and go.
Why Pod IPs are not enough
Each Pod gets an IP address inside the cluster. But that IP belongs to the Pod lifecycle.
If the Pod is deleted and replaced, the replacement Pod usually gets a different IP address.
That means this is a bad design:
Application A connects directly to Pod IP 10.244.0.12
If that Pod disappears, the IP may disappear with it.
This is better:
Application A connects to Service name nginx-service-lab
The Service remains stable while Kubernetes updates which Pods are behind it.
Services, labels, and selectors
Services usually find Pods through labels and selectors.
A Pod may have a label like:
app: nginx-service-lab
A Service may have a selector like:
selector: app: nginx-service-lab
That selector means:
Send traffic to Pods with the label app=nginx-service-lab.
What are endpoints?
Endpoints represent the actual network destinations behind a Service.
If a Service selects three healthy Pods, the Service should have endpoints pointing to those Pods.
If a Service has no endpoints, traffic has nowhere to go.
You can check endpoints with:
kubectl get endpoints
Or for a specific Service:
kubectl describe service <service-name>
Common Service types
Kubernetes supports several Service types. These are the ones to understand first:
- ClusterIP: Exposes the Service inside the cluster. This is the default.
- NodePort: Exposes the Service on a port across cluster nodes.
- LoadBalancer: Requests an external load balancer from the cloud provider.
- ExternalName: Maps a Service to an external DNS name.
In this local kind lab, we will focus on ClusterIP and briefly look at NodePort. LoadBalancer behavior depends on cloud or local load balancer support, so it comes later.
Before you start the lab
This lab assumes:
- Docker Desktop is running
- Your kind cluster exists
- kubectl is installed
- Your current context is kind-stealth-k8s-lab
Check your context:
kubectl config current-context
You should see:
kind-stealth-k8s-lab
Check your node:
kubectl get nodes
If your cluster no longer exists, recreate it:
kind create cluster --name stealth-k8s-lab
Step 1: Create a Deployment
Start by creating an nginx Deployment:
kubectl create deployment nginx-service-lab --image=nginx:latest
Scale it to three replicas:
kubectl scale deployment nginx-service-lab --replicas=3
Check the Deployment and Pods:
kubectl get deployments kubectl get pods -o wide
You should see three Pods. Each Pod has its own IP address.
Step 2: Look at the Pod labels
Run:
kubectl get pods --show-labels
You should see a label like:
app=nginx-service-lab
That label is how the Service will know which Pods to route to.
Step 3: Create a ClusterIP Service
Create a Service for the Deployment:
kubectl expose deployment nginx-service-lab --port=80 --target-port=80 --type=ClusterIP
This creates a Service named nginx-service-lab.
What this command does
- Creates a Service for the Deployment
- Uses port 80 on the Service
- Sends traffic to target port 80 on the Pods
- Uses the default internal Service type, ClusterIP
Step 4: Inspect the Service
Run:
kubectl get services
You should see something like:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 20m nginx-service-lab ClusterIP 10.96.xxx.xxx <none> 80/TCP 30s
What to look for
- NAME: The Service name.
- TYPE: The Service type.
- CLUSTER-IP: The stable internal IP assigned to the Service.
- PORT(S): The port exposed by the Service.
The ClusterIP is stable for the life of the Service, but it is only reachable from inside the cluster unless you use a tool like port-forward.
Step 5: Describe the Service
Run:
kubectl describe service nginx-service-lab
Look for:
- Selector
- IP
- Port
- TargetPort
- Endpoints
The endpoints should show one or more Pod IPs and ports. Those are the actual destinations behind the Service.
Step 6: Check endpoints directly
Run:
kubectl get endpoints nginx-service-lab
You should see endpoint IPs for the selected Pods.
If the endpoint list is empty, the Service is not matching any Pods.
Step 7: Test the Service with port-forward
Use port-forwarding to test the Service from your local machine:
kubectl port-forward service/nginx-service-lab 8080:80
Keep that PowerShell window open. Then open a browser and go to:
http://localhost:8080
You should see the nginx welcome page.
Press Ctrl+C in PowerShell to stop port-forwarding.
Step 8: Delete a Pod and test again
Get the Pods:
kubectl get pods
Delete one Pod. Replace <pod-name> with an actual Pod name:
kubectl delete pod <pod-name>
Check Pods again:
kubectl get pods -o wide
The Deployment should create a replacement Pod.
Check the Service endpoints again:
kubectl get endpoints nginx-service-lab
The endpoint list may change as the replacement Pod comes up.
This is the point of a Service: the backend Pods can change, but the Service name and Service IP remain stable.
Step 9: Create a Service from YAML
Create a file named:
service-lab.yaml
Paste this into the file:
apiVersion: v1
kind: Service
metadata:
name: nginx-yaml-service-lab
spec:
type: ClusterIP
selector:
app: nginx-service-lab
ports:
- port: 80
targetPort: 80
protocol: TCP
Apply it:
kubectl apply -f service-lab.yaml
Check Services:
kubectl get services
You now have another Service selecting the same nginx Pods.
Step 10: Understand port vs targetPort
In the YAML, this section matters:
ports: - port: 80 targetPort: 80 protocol: TCP
The difference is:
- port: The port exposed by the Service.
- targetPort: The port on the selected Pods where traffic should be sent.
In this case, both are 80. They do not always need to match.
Step 11: Create a broken Service on purpose
Create a file named:
broken-service-lab.yaml
Paste this into the file:
apiVersion: v1
kind: Service
metadata:
name: broken-service-lab
spec:
type: ClusterIP
selector:
app: does-not-match-any-pods
ports:
- port: 80
targetPort: 80
protocol: TCP
Apply it:
kubectl apply -f broken-service-lab.yaml
Check the Service:
kubectl get service broken-service-lab
Now check endpoints:
kubectl get endpoints broken-service-lab
You should see no endpoints.
Why it is broken
The Service selector is:
app: does-not-match-any-pods
But the Pods are labeled:
app=nginx-service-lab
The selector does not match the labels, so the Service has no backend Pods.
Step 12: Fix the broken Service
Edit broken-service-lab.yaml and change:
selector: app: does-not-match-any-pods
to:
selector: app: nginx-service-lab
Apply the file again:
kubectl apply -f broken-service-lab.yaml
Check endpoints again:
kubectl get endpoints broken-service-lab
You should now see endpoints.
Step 13: Try a NodePort Service
Create a NodePort Service from the Deployment:
kubectl expose deployment nginx-service-lab --name=nginx-nodeport-lab --port=80 --target-port=80 --type=NodePort
Check the Service:
kubectl get service nginx-nodeport-lab
You should see a port mapping that looks something like:
80:30xxx/TCP
The high port is the NodePort assigned by Kubernetes.
In real clusters, NodePort exposes the Service on the nodes. In kind, direct access can depend on how the cluster was created and how Docker networking is mapped, so do not worry if browsing directly to the NodePort does not work in this local lab.
For this training path, use port-forward for reliable local testing until we get into Ingress and cloud load balancers.
Step 14: Clean up
Delete the Services:
kubectl delete service nginx-service-lab kubectl delete -f service-lab.yaml kubectl delete -f broken-service-lab.yaml kubectl delete service nginx-nodeport-lab
Delete the Deployment:
kubectl delete deployment nginx-service-lab
Confirm cleanup:
kubectl get services kubectl get deployments kubectl get pods kubectl get endpoints
You should not see the lab Deployment, Pods, or Services anymore.
Common Service troubleshooting commands
Use this flow when a Service does not work:
kubectl get services kubectl describe service <service-name> kubectl get endpoints <service-name> kubectl get pods --show-labels kubectl get pods -o wide kubectl describe pod <pod-name> kubectl logs <pod-name> kubectl get events --sort-by=.metadata.creationTimestamp
This usually tells you:
- Whether the Service exists
- What selector the Service uses
- Whether the Service has endpoints
- Whether the Pod labels match the Service selector
- Whether the selected Pods are running
- Whether the application is actually listening on the target port
What you just learned
In this lesson, you:
- Created a Deployment with three Pods
- Inspected Pod labels
- Created a ClusterIP Service
- Inspected Service details
- Checked endpoints
- Tested a Service with port-forwarding
- Deleted a Pod and watched the Service continue to represent the workload
- Created a Service from YAML
- Broke a Service with a bad selector
- Fixed a Service by matching selectors to labels
- Created a NodePort Service
- Cleaned up the lab resources
Check your understanding
Use these questions to test whether the Service concepts made sense.
- Why is connecting directly to a Pod IP usually a bad idea?
- What problem does a Service solve?
- How does a Service usually find the Pods it should send traffic to?
- What are endpoints?
- What does a ClusterIP Service expose?
- What is the difference between port and targetPort?
- What is the first thing to check when a Service has no endpoints?
- What command shows the labels on Pods?
- What command shows endpoints for a Service?
- Why might NodePort behavior be different in a local kind lab than in a real cluster?
- What tool can you use to test a Service locally without exposing it externally?
- What are three commands you would run when troubleshooting a Service?
Answer key
- Pod IPs belong to individual Pods and can change when Pods are replaced.
- A Service provides stable network access to matching Pods.
- Through labels and selectors.
- Endpoints are the actual Pod IPs and ports behind a Service.
- It exposes a stable internal Service IP inside the cluster.
- port is the port exposed by the Service. targetPort is the port on the selected Pods.
- Check whether the Service selector matches the Pod labels.
- kubectl get pods --show-labels.
- kubectl get endpoints <service-name>.
- kind runs nodes as Docker containers, so node-level port access depends on how the local cluster and Docker networking are configured.
- kubectl port-forward.
- Examples: kubectl describe service <service-name>, kubectl get endpoints <service-name>, kubectl get pods --show-labels, kubectl get pods -o wide.