Here is a problem with everything we built in Part 3. Yes, our Deployment keeps three nginx pods running and self-heals when they crash. But every time a pod is recreated, it gets a brand new IP address. If you have a frontend application trying to send requests to the backend, the IP it knew about is now stale. The pod is gone, replaced by a new one with a different address.
This is not a theoretical problem. It is the fundamental networking challenge of containerised environments. Before Kubernetes, people solved this by registering services in external tools like Consul or Zookeeper. Kubernetes solves it natively through Services.
A Kubernetes Service is a stable virtual IP and DNS name that sits in front of a set of pods. It never changes, even as pods come and go. All traffic going to the Service gets automatically load-balanced across the healthy pods behind it. This is how every part of your application finds and talks to every other part.
Every pod in Kubernetes gets its own IP address. Pods on the same node can communicate directly. Pods on different nodes use an overlay network — Kubernetes automatically handles routing between nodes. From a pod's perspective, every other pod in the cluster is reachable by IP, no matter which node it is on.
But pod IPs are not stable — they are assigned from a pool and change when pods restart. Services solve this by providing a stable virtual IP (called a ClusterIP) that stays constant, with Kubernetes automatically updating the routing behind the scenes whenever the pods change.
Kubernetes has three main Service types, each suited to different use cases. Understanding which to use is essential for designing production applications.
ClusterIP — The default type. Creates a virtual IP that is only accessible from within the cluster. Used for internal communication between services — for example, your frontend pods talking to your backend pods, or your backend talking to Redis. External users cannot reach a ClusterIP Service directly.
NodePort — Opens a specific port on every node in the cluster and routes traffic from that port to the Service. This makes your application accessible from outside the cluster using any node's IP and the assigned port. Ports are in the range 30000–32767. NodePort is good for development and testing but not ideal for production because it exposes ports directly on your nodes.
LoadBalancer — When running in a cloud environment (AWS, GCP, Azure), this type automatically provisions a cloud load balancer with a public IP and routes traffic to your pods. This is the standard way to expose production applications in the cloud. On Minikube, you need to run minikube tunnel for LoadBalancer to work.
Let us create a Deployment with a ClusterIP Service to understand internal pod communication.
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: nginx:1.25
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 80
targetPort: 80
kubectl apply -f backend-deployment.yaml
kubectl get services
kubectl describe service backend-service
# Get the ClusterIP assigned
kubectl get service backend-service -o jsonpath='{.spec.clusterIP}'
The selector: app: backend in the Service definition is critical. This is how the Service knows which pods to route traffic to — it looks for pods with the matching label. When you add or remove pods from the Deployment, the Service automatically includes or excludes them based on this label selector.
NodePort lets you access your application from outside the cluster. This is what we use during local development with Minikube.
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30080 # Optional: specify the port, or let Kubernetes assign one
kubectl apply -f frontend-service.yaml
# Get the URL to access the service in Minikube
minikube service frontend-service --url
# Or open it directly in browser
minikube service frontend-service
This is one of my favourite features in Kubernetes. When you create a Service, Kubernetes automatically registers it in the cluster DNS. Any pod in the cluster can reach that service by its name — no IP addresses needed, no configuration files, nothing.
If you have a service named backend-service in the default namespace, any pod in the same namespace can reach it at simply backend-service. The full DNS name is backend-service.default.svc.cluster.local, but within the same namespace you can just use the short name.
# Run a temporary pod to test connectivity
kubectl run test-dns --image=busybox --rm -it -- /bin/sh
# Inside the pod, try to reach the backend service by name
wget -qO- http://backend-service
# Try the full DNS name
wget -qO- http://backend-service.default.svc.cluster.local
# Exit the pod (it deletes itself because of --rm)
exit
The fact that services find each other by name rather than IP is what makes microservices architecture in Kubernetes so clean. Your frontend code just calls http://backend-service/api and Kubernetes handles all the routing. This is the same in development and production — no environment variables needed for internal service URLs.
When a Service routes traffic to pods, it uses an Endpoints object. Kubernetes automatically creates and maintains an Endpoints object for each Service, listing the IP addresses and ports of all healthy pods that match the Service's selector.
# See the endpoints (pod IPs) behind a service
kubectl get endpoints backend-service
# Watch endpoints change as pods come and go
kubectl get endpoints backend-service -w
Run this and then delete one of your backend pods in another terminal. You will see the endpoint list update immediately, removing the deleted pod's IP. This is how the Service always routes to healthy pods only.
# Create LoadBalancer service
kubectl expose deployment backend --type=LoadBalancer --port=80
# In Minikube, run tunnel to assign an external IP (run in separate terminal)
minikube tunnel
# Check the external IP assignment
kubectl get service backend
# EXTERNAL-IP should now show an IP instead of
# List all services
kubectl get services
# Describe a service in detail
kubectl describe service backend-service
# Delete a service
kubectl delete service backend-service
# Expose an existing deployment as a service (shortcut)
kubectl expose deployment my-app --type=NodePort --port=80
# Edit a service in place
kubectl edit service backend-service
# Get service as YAML
kubectl get service backend-service -o yaml
You now understand the networking layer of Kubernetes. You know how pods communicate internally using ClusterIP Services and DNS, how to expose applications externally with NodePort and LoadBalancer, and how Kubernetes keeps service endpoints up to date as pods change.
But there is still something missing. Your pods are currently getting their configuration — database URLs, API keys, feature flags — baked into the container image or hardcoded. That is terrible practice. In Part 5, we cover ConfigMaps and Secrets — the proper way to manage application configuration and sensitive data in Kubernetes.
Pod IP addresses change every time a pod restarts. Services provide a stable virtual IP and DNS name that never changes, giving other pods a reliable address to communicate with regardless of how often the underlying pods are replaced.
ClusterIP is internal-only (pod-to-pod). NodePort opens a port on every cluster node for external access. LoadBalancer provisions a cloud load balancer with a public IP — the standard for production external traffic in cloud environments.
kube-proxy on each node uses iptables or IPVS rules to route Service traffic to pods using round-robin by default. Unhealthy pods are automatically removed from rotation, so only working pods receive traffic.
CoreDNS automatically creates a DNS record for every Service. Pods in the same namespace can reach a service by its short name. Cross-namespace requires the full name: service-name.namespace.svc.cluster.local. No manual configuration needed.
Use NodePort for simple external access (good for dev), LoadBalancer in cloud environments for production, or Ingress (covered in Part 8) which is the recommended production approach for routing multiple services through one load balancer.