Kubernetes Core Concepts

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
Field note: A Service does not run your application. It gives stable network access to Pods that match its selector.

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.
Field note: If a Service has no endpoints, the first thing to check is usually whether the Service selector matches the Pod labels.

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.

Field note: A Service with no endpoints is like a load balancer with no healthy backends. The Service exists, but traffic has nowhere useful to go.

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.

Field note: When troubleshooting Services, always compare the Service selector to the labels on the Pods.

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
Bottom line: Pods are replaceable. Services give those replaceable Pods a stable network identity. When a Service is not working, check its selector, check the Pod labels, and check the endpoints.

Check your understanding

Use these questions to test whether the Service concepts made sense.

  1. Why is connecting directly to a Pod IP usually a bad idea?
  2. What problem does a Service solve?
  3. How does a Service usually find the Pods it should send traffic to?
  4. What are endpoints?
  5. What does a ClusterIP Service expose?
  6. What is the difference between port and targetPort?
  7. What is the first thing to check when a Service has no endpoints?
  8. What command shows the labels on Pods?
  9. What command shows endpoints for a Service?
  10. Why might NodePort behavior be different in a local kind lab than in a real cluster?
  11. What tool can you use to test a Service locally without exposing it externally?
  12. What are three commands you would run when troubleshooting a Service?
Answer key
  1. Pod IPs belong to individual Pods and can change when Pods are replaced.
  2. A Service provides stable network access to matching Pods.
  3. Through labels and selectors.
  4. Endpoints are the actual Pod IPs and ports behind a Service.
  5. It exposes a stable internal Service IP inside the cluster.
  6. port is the port exposed by the Service. targetPort is the port on the selected Pods.
  7. Check whether the Service selector matches the Pod labels.
  8. kubectl get pods --show-labels.
  9. kubectl get endpoints <service-name>.
  10. kind runs nodes as Docker containers, so node-level port access depends on how the local cluster and Docker networking are configured.
  11. kubectl port-forward.
  12. Examples: kubectl describe service <service-name>, kubectl get endpoints <service-name>, kubectl get pods --show-labels, kubectl get pods -o wide.
← Previous: Deployments and ReplicaSets Series index Next: ConfigMaps and Secrets →