Article

Kubernetes Services & Networking: How Pods Talk to the World

10 min read
Originally published on Medium. Read, clap or comment on Medium ↗

Pods are ephemeral. Their IPs change constantly. Services are how Kubernetes solves the oldest problem in distributed systems — finding what you need, reliably, every time.


Kubernetes Zero to Hero · Episode 04


In Episode 03, we built confidence with Pods, ReplicaSets, and Deployments. You can now create workloads, roll updates, and watch Kubernetes self-heal in real time.

But here’s the problem: those pods can’t talk to anyone.

Not to the outside world. Not reliably to each other. Because every time a pod restarts — and pods restart all the time — it gets a brand new IP address. If your frontend hardcodes the IP of your backend, it breaks the moment that backend pod is rescheduled. This is the fundamental networking challenge in Kubernetes, and Services are the solution.

This episode covers everything you need to go from isolated pods to a fully connected, production-ready application network.


The Core Problem: Pods Are Mortal, IPs Are Temporary

Let’s make the problem concrete.

You have a frontend deployment with 3 pods and a backend deployment with 3 pods. The frontend needs to call the backend. Simple enough — but which IP does it use?

# Backend pods and their IPs today
$ kubectl get pods -o wide
NAME IP
backend-7d4b9c-x2k9f 10.244.1.5
backend-7d4b9c-p3mzr 10.244.2.3
backend-7d4b9c-hn7wq 10.244.3.7
# Tomorrow, after a rolling update:
backend-9c8e4a-aabb1 10.244.1.14 ← completely different IPs
backend-9c8e4a-ccdd2 10.244.2.19
backend-9c8e4a-eeff3 10.244.3.2

Hardcoding pod IPs is hopeless. You need a stable address that survives pod restarts, rolling updates, and rescheduling. That’s exactly what a Service provides.


What Is a Kubernetes Service?

A Service is a stable network abstraction in front of a dynamic set of pods. It has:

When pods come and go, the Service’s IP stays constant. The Endpoints controller (from Episode 02) watches for pods matching the Service’s selector and continuously updates the list of healthy backends. kube-proxy programs the actual network routing rules on every node.

Frontend Pod
│
│ calls backend.default.svc.cluster.local:8080
▼
Service (ClusterIP: 10.96.45.100, Port: 8080) ← stable, never changes
│
├──▶ Backend Pod A (10.244.1.5:8080)
├──▶ Backend Pod B (10.244.2.3:8080)
└──▶ Backend Pod C (10.244.3.7:8080)

The Four Service Types

Kubernetes has four Service types, each solving a different exposure problem. Understanding when to use each one is essential.


Type 1: ClusterIP — Internal Traffic Only

Default type. Creates a virtual IP accessible only from within the cluster. Nothing outside the cluster can reach it.

Use it for: Any service that should only be reachable by other pods — databases, internal APIs, caches, message queues.

# clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
type: ClusterIP # default — can omit this line
selector:
app: backend # routes to pods with this label
ports:
- protocol: TCP
port: 8080 # port the Service listens on
targetPort: 8080 # port the container listens on
$ kubectl apply -f clusterip-service.yaml
$ kubectl get service backend-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
backend-service ClusterIP 10.96.45.100 <none> 8080/TCP 5s# Test from inside the cluster — exec into any pod
$ kubectl exec -it <any-pod> -- curl http://backend-service:8080
# Or use the full DNS name:
$ kubectl exec -it <any-pod> -- curl http://backend-service.default.svc.cluster.local:8080

💡 DNS naming convention: <service-name>.<namespace>.svc.cluster.local Within the same namespace, the short name <service-name> resolves fine. Across namespaces, use the full name.


Type 2: NodePort — Expose on Every Node’s IP

Opens a specific port on every node in the cluster and forwards traffic from <NodeIP>:<NodePort> to the Service.

Use it for: Development, testing, and simple on-premises setups where you don’t have a cloud load balancer. Rarely used in production on cloud platforms.

# nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- protocol: TCP
port: 80 # ClusterIP port (internal)
targetPort: 80 # container port
nodePort: 30080 # port opened on every node (30000–32767)
$ kubectl apply -f nodeport-service.yaml
$ kubectl get service web-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-nodeport NodePort 10.96.12.200 <none> 80:30080/TCP 5s# Access via any node's IP on port 30080
# With minikube:
$ minikube service web-nodeport --url
http://192.168.49.2:30080

⚠️ NodePort range is 30000–32767. If you don’t specify nodePort, Kubernetes picks one randomly. In production, the random port assignment makes NodePort awkward to use — you'd need to configure your firewall or external load balancer to forward to it.


Type 3: LoadBalancer — Cloud-Native External Access

Provisions an external load balancer from your cloud provider (AWS ELB, GCP Load Balancer, Azure Load Balancer) and assigns it a public IP. This is the standard way to expose a service to the internet on managed Kubernetes platforms (EKS, GKE, AKS).

Use it for: Production services that need a stable public endpoint on a cloud platform.

# loadbalancer-service.yaml
apiVersion: v1
kind: Service
metadata:
name: web-loadbalancer
spec:
type: LoadBalancer
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 80
$ kubectl apply -f loadbalancer-service.yaml
$ kubectl get service web-loadbalancer
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-loadbalancer LoadBalancer 10.96.78.50 34.120.45.210 80:31234/TCP 30s
# ↑ public IP assigned by cloud provider

⚠️ Cost warning: Every LoadBalancer Service provisions a separate cloud load balancer. In a large cluster with many services, this gets expensive fast. In Episode 07 (Ingress), we’ll show you how to use a single load balancer for all your services — the right way to do it at scale.


Type 4: ExternalName — Map to an External DNS Name

A special type that maps a Service name to an external DNS name. No proxying, no load balancing — purely a DNS alias.

Use it for: Migrating workloads that need to reference an external database or API without hardcoding the hostname in every pod.

apiVersion: v1
kind: Service
metadata:
name: external-db
namespace: production
spec:
type: ExternalName
externalName: mydb.example.com # external DNS name
# Pods can now reach your external DB via the stable internal name:
# external-db.production.svc.cluster.local
# → resolves to mydb.example.com

Service Types at a Glance

Type → Accessible From → Use Case
ClusterIP → Inside cluster only → Internal microservice communication NodePort → Node IP + port → Dev/test, simple on-prem exposure LoadBalancer → Public internet → Production cloud services (one LB per service)
ExternalName → Inside cluster only → Alias for external services


DNS in Kubernetes: How Pods Find Each Other

Every Kubernetes cluster runs CoreDNS — a DNS server that automatically creates DNS records for every Service. This is the magic that lets your frontend call http://backend-service:8080 instead of an IP address.

How DNS resolution works

When a pod makes a DNS query, it goes to CoreDNS (running in kube-system). CoreDNS resolves Service names to their ClusterIP:

Pod queries:    backend-service
CoreDNS checks: Is there a Service named "backend-service" in namespace "default"?
CoreDNS returns: 10.96.45.100 (the ClusterIP)
Pod connects: 10.96.45.100:8080

The DNS search path

Each pod is configured with a DNS search path:

# Check the DNS config inside any pod
$ kubectl exec -it <pod-name> -- cat /etc/resolv.conf
nameserver 10.96.0.10           # CoreDNS ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local

This search path means backend-service automatically expands to backend-service.default.svc.cluster.local. You only need the full name when crossing namespaces.

# Same namespace — short name works
curl http://backend-service:8080
# Different namespace — use full name
curl http://backend-service.payments.svc.cluster.local:8080# Verify DNS from inside a pod (useful for debugging)
$ kubectl exec -it <pod-name> -- nslookup backend-service
Server: 10.96.0.10
Address: 10.96.0.10#53Name: backend-service.default.svc.cluster.local
Address: 10.96.45.100

Endpoints: The Bridge Between Services and Pods

When you create a Service, Kubernetes automatically creates a matching Endpoints object that tracks the IPs of all healthy pods matching the selector.

# See the Endpoints for a Service
$ kubectl get endpoints backend-service
NAME ENDPOINTS AGE
backend-service 10.244.1.5:8080,10.244.2.3:8080,10.244.3.7:8080 2m
# Watch Endpoints change in real time as pods come and go
$ kubectl get endpoints backend-service --watch

💡 Debugging tip: If traffic isn’t reaching your pods, check the Endpoints object first. Empty Endpoints (<none>) means no pods are matching your Service selector — usually a label mismatch. This is the #1 cause of "Service not working" issues.

# Verify your service selector matches your pod labels
$ kubectl get service backend-service -o yaml | grep -A 3 selector
$ kubectl get pods --show-labels | grep backend

Putting It Together: A Complete Frontend + Backend Example

Let’s deploy a real two-tier application — a frontend that calls a backend — with proper Services wiring them together.

# backend-deployment.yaml
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: hashicorp/http-echo:0.2.3
args:
- "-text=Hello from the backend!"
ports:
- containerPort: 5678
---
# backend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 80
targetPort: 5678
---
# frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: nginx:1.25
ports:
- containerPort: 80
---
# frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30080
# Deploy everything
$ kubectl apply -f .
# Verify all pods are running
$ kubectl get pods
$ kubectl get services# Test backend connectivity from a frontend pod
$ kubectl exec -it <frontend-pod> -- curl http://backend-service:80
Hello from the backend!# Access the frontend externally (minikube)
$ minikube service frontend-service --url

NetworkPolicy: Controlling Who Talks to Whom

By default, every pod in a Kubernetes cluster can talk to every other pod. In production, that’s a security problem. NetworkPolicy lets you define firewall rules at the pod level.

# deny-all-ingress.yaml — deny all inbound traffic by default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: default
spec:
podSelector: {} # applies to ALL pods in the namespace
policyTypes:
- Ingress
# allow-backend-from-frontend.yaml — only allow frontend to reach backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
spec:
podSelector:
matchLabels:
app: backend # this policy applies to backend pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # only allow traffic from frontend pods
ports:
- protocol: TCP
port: 5678

⚠️ NetworkPolicy requires a CNI plugin that supports it — Calico, Cilium, or Weave Net. Flannel does not support NetworkPolicy. If you’re on a managed cluster (EKS, GKE, AKS), check which CNI is installed before writing NetworkPolicies and expecting them to be enforced.


Common Networking Failures and How to Debug Them

Service not reachable from a pod

# Step 1: Does the Service exist?
$ kubectl get service <service-name>
# Step 2: Does the Service have Endpoints? (empty = label mismatch)
$ kubectl get endpoints <service-name># Step 3: Are the pods actually healthy and Running?
$ kubectl get pods -l app=<selector-label># Step 4: Can you curl directly to a pod IP (bypassing the Service)?
$ kubectl get pods -o wide # get pod IP
$ kubectl exec -it <other-pod> -- curl http://<pod-ip>:<port>
# If this works but the Service doesn't → Service config issue
# If this doesn't work either → app or pod issue

DNS not resolving

# Test DNS resolution from inside a pod
$ kubectl exec -it <pod-name> -- nslookup kubernetes.default
$ kubectl exec -it <pod-name> -- nslookup <service-name>
# Check if CoreDNS pods are healthy
$ kubectl get pods -n kube-system -l k8s-app=kube-dns# Check CoreDNS logs for errors
$ kubectl logs -n kube-system -l k8s-app=kube-dns

Traffic going to the wrong pods

# Verify your Service selector exactly matches pod labels
$ kubectl describe service <service-name> # check Selector field
$ kubectl get pods --show-labels # check pod labels
# Even one label key/value mismatch = zero traffic routed

Hands-On Lab

# 1. Deploy the two-tier app from the example above
$ kubectl apply -f .
# 2. Verify endpoints are populated
$ kubectl get endpoints backend-service# 3. Test internal DNS from a pod
$ kubectl exec -it <frontend-pod> -- nslookup backend-service# 4. Test connectivity through the Service
$ kubectl exec -it <frontend-pod> -- curl http://backend-service# 5. Deliberately break it — change a label and watch endpoints drain
$ kubectl label pod <backend-pod> app=broken --overwrite
$ kubectl get endpoints backend-service # one less endpoint# 6. Fix it
$ kubectl label pod <backend-pod> app=backend --overwrite
$ kubectl get endpoints backend-service # back to normal# 7. Apply the deny-all NetworkPolicy and test that connectivity breaks
# 8. Apply the allow policy and test that it's restored# 9. Clean up
$ kubectl delete -f .

The label manipulation in steps 5 and 6 is one of the most instructive exercises in this series. It makes the Endpoints mechanism completely tangible.


What’s Next

In Episode 05, we tackle ConfigMaps and Secrets — how to manage configuration and sensitive data outside your container images. We’ll cover how to inject config as environment variables and files, how Secrets differ from ConfigMaps, and the security practices that separate a careful operator from a careless one.

Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 Pods, Deployments & ReplicaSets
EP 04 ← You are here Services & Networking
EP 05 ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
EP 07 Ingress Controllers & Load Balancing
EP 08 RBAC & Security Fundamentals
EP 09 Monitoring with Prometheus & Grafana
EP 10 Production Hardening & Helm Charts


Before the Next Article

Make sure you can answer these from memory:

If you worked through the hands-on lab and broke things deliberately in steps 5 and 6 — those answers should come naturally.


Networking is the piece most engineers learn last and struggle with most. You’ve just learned it early. That’s an edge you’ll feel every time something breaks in production and you know exactly where to look. See you in Episode 05.

← All articles Discuss on Medium ↗