Networking

Services — ClusterIP, NodePort, LoadBalancer

● Intermediate ⏱ 15 min read

Pods are ephemeral — they get new IP addresses every time they restart. A Service is a stable network endpoint that abstracts over a dynamic set of pods. It provides a fixed virtual IP (ClusterIP) and DNS name, load-balances traffic across all matching pods, and automatically updates its endpoint list as pods come and go. Services are how everything in Kubernetes talks to everything else.

What Is a Service?

A Service selects pods using a label selector — the same mechanism you use with kubectl get pods -l app=nginx. Any pod with matching labels is automatically included in the Service's endpoint list. Any pod that no longer matches (deleted, label removed, readiness probe failing) is removed.

The four Service types, in order of increasing external accessibility:

TypeAccessible fromUse case
ClusterIPInside the cluster onlyInternal microservice communication
NodePortOutside via any node's IP + portDevelopment, bare-metal clusters, quick external access
LoadBalancerOutside via a cloud load balancer IPProduction external traffic (cloud providers)
ExternalNameDNS alias for an external hostnamePoint a cluster-internal name at an external service

How Services Work

When you create a Service, the control plane assigns it a virtual IP (the ClusterIP) from a reserved range. This IP is not assigned to any real network interface — it only exists in iptables or IPVS rules that kube-proxy programs on every node.

When a pod sends a packet to the ClusterIP, kube-proxy's rules intercept it in the kernel, randomly select one of the Service's healthy endpoints, and rewrite the destination to that pod's real IP. The response comes back through the same NAT path.

Traffic flow through a ClusterIP Service
client pod
→ 10.96.0.10:80
kube-proxy (iptables)
DNAT to pod IP
backend pod
10.244.1.5:8080
ClusterIP 10.96.0.10 is virtual — exists only in iptables rules on every node
kube-proxy rewrites traffic from the virtual ClusterIP to a real pod IP using DNAT rules

ClusterIP

ClusterIP is the default Service type. It creates a virtual IP reachable only from within the cluster. Other pods resolve the Service by its DNS name: <service-name>.<namespace>.svc.cluster.local.

clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: default
spec:
  type: ClusterIP    # default — can be omitted
  selector:
    app: api
  ports:
  - name: http
    port: 80         # port the Service listens on
    targetPort: 8080 # port on the pod

DNS resolution from within the same namespace: http://api. From a different namespace: http://api.default.svc.cluster.local.

NodePort

A NodePort Service extends ClusterIP by also opening a port on every node's external IP (range 30000–32767 by default). Traffic to <any-node-ip>:<nodePort> is forwarded to the Service's ClusterIP and then to a pod.

nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: NodePort
  selector:
    app: api
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 31080    # optional — auto-assigned if omitted (30000–32767)
⚠️
NodePort is not for production external traffic

NodePort exposes a high-numbered port on every node. Users must know a node's IP. If that node goes down, the entry point is gone. In production, use LoadBalancer or an Ingress controller backed by a load balancer — NodePort is for local clusters, CI environments, or bare-metal setups with an external load balancer you manage yourself.

LoadBalancer

A LoadBalancer Service provisions an external load balancer from your cloud provider (AWS ELB, GCP LB, Azure LB) and assigns it a public IP. Traffic to that IP is forwarded to the Service's NodePort and then to pods. It's the simplest way to expose a service to the internet on cloud Kubernetes.

loadbalancer-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api
  annotations:
    # Cloud-provider-specific annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
  - port: 443
    targetPort: 8080

After creation, kubectl get service api shows an EXTERNAL-IP once the cloud provider provisions the load balancer (takes 30–90 seconds).

💡
One LoadBalancer = one cloud LB = one cost

Each LoadBalancer Service provisions a separate cloud load balancer, which costs money and has its own IP. For hosting multiple HTTP services, use a single Ingress controller backed by one LoadBalancer Service, then route via host/path rules. This is significantly cheaper and more manageable at scale.

ExternalName

An ExternalName Service maps a cluster-internal name to an external DNS hostname. No proxying happens — the service returns a CNAME record. Useful for referencing external services (managed databases, SaaS APIs) by a stable in-cluster name.

apiVersion: v1
kind: Service
metadata:
  name: database
  namespace: production
spec:
  type: ExternalName
  externalName: mydb.example.com    # external hostname

Pods can then connect to database.production.svc.cluster.local and the DNS resolves to mydb.example.com. Switching from an external database to an in-cluster one only requires changing the Service — no application config changes.

Headless Services

Setting clusterIP: None creates a headless Service — no VIP is assigned. Instead, DNS returns A records for each individual pod IP. This is required for StatefulSets where pods need to be addressed individually.

apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  clusterIP: None    # headless
  selector:
    app: mysql
  ports:
  - port: 3306

With a headless Service, nslookup mysql.default.svc.cluster.local returns multiple A records — one per pod. mysql-0.mysql.default.svc.cluster.local resolves to the IP of pod mysql-0 specifically.

Selectors & Endpoints

A Service with a selector automatically manages an Endpoints object — a list of pod IPs and ports. You can inspect it:

# See which pods a Service is routing to
kubectl get endpoints api

# NAME   ENDPOINTS                        AGE
# api    10.244.1.5:8080,10.244.2.3:8080  5m

Services without selectors don't auto-manage Endpoints — you manage them manually. This lets you create a Service that routes to an external IP or to pods selected by arbitrary criteria.

kubectl Commands

# Apply a Service
kubectl apply -f service.yaml

# List Services
kubectl get services
kubectl get svc    # short form

# Describe a Service (shows endpoints, selector, events)
kubectl describe service api

# Get the ClusterIP
kubectl get service api -o jsonpath='{.spec.clusterIP}'

# Get the external IP of a LoadBalancer Service
kubectl get service api -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

# Expose a Deployment quickly (imperative)
kubectl expose deployment api --port=80 --target-port=8080

# Port-forward to a Service for local testing
kubectl port-forward service/api 8080:80

# Delete a Service
kubectl delete service api