Kubernetes Service objects can abstract away the complexities of Pod networking, but the way they distribute traffic isn’t always obvious.
Let’s see ClusterIP and NodePort in action. Imagine we have a simple web application running in Kubernetes.
First, we deploy our application as Pods. Each Pod gets its own IP address, but these are ephemeral and internal to the cluster.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-webapp
spec:
replicas: 3
selector:
matchLabels:
app: my-webapp
template:
metadata:
labels:
app: my-webapp
spec:
containers:
- name: web
image: nginx:latest
ports:
- containerPort: 80
Now, we create a ClusterIP Service. This Service will get its own stable IP address within the cluster.
apiVersion: v1
kind: Service
metadata:
name: my-webapp-clusterip
spec:
selector:
app: my-webapp
ports:
- protocol: TCP
port: 80
targetPort: 80
When you access my-webapp-clusterip from another Pod inside the cluster, Kubernetes’ kube-proxy component intercepts this traffic. It randomly selects one of the healthy my-webapp Pods and forwards the request. This is basic round-robin load balancing. The ClusterIP itself is a virtual IP, managed by kube-proxy.
Now, what if we want to expose this application outside the cluster? We can use a NodePort Service. This builds upon ClusterIP.
apiVersion: v1
kind: Service
metadata:
name: my-webapp-nodeport
spec:
type: NodePort
selector:
app: my-webapp
ports:
- protocol: TCP
port: 80 # The port the Service is available on within the cluster
targetPort: 80 # The port your application is listening on in the Pods
nodePort: 30080 # The static port exposed on each Node
With NodePort, Kubernetes does two things:
- It creates a
ClusterIPService (you can see this if youkubectl get svc my-webapp-nodeport -o yamland look for theclusterIPsfield). - It opens a specific port (in this case,
30080) on every single Node in the cluster.
You can then access your application by hitting ANY_NODE_IP:30080. When a request hits ANY_NODE_IP:30080, kube-proxy on that Node intercepts it. It then consults the Service’s endpoints (the IPs of the my-webapp Pods) and forwards the traffic to one of them, using the targetPort (80).
The port field in the Service definition (80 in the NodePort example) is the port that the Service is available on internally within the cluster via its ClusterIP. The nodePort field is the external port that is opened on each Node. If you don’t specify nodePort, Kubernetes will assign a random one from the ephemeral port range (typically 30000-32767).
If you want a more robust external access solution, you’d typically use a LoadBalancer Service type (which provisions an external cloud load balancer) or an Ingress controller. NodePort is often used for development or simple scenarios.
The surprising part is how kube-proxy actually achieves this. It’s not magic; it’s typically implemented using iptables rules (or ipvs on newer versions) on each Node. When a Service is created or updated, kube-proxy programs these iptables rules to intercept traffic destined for the Service’s ClusterIP or NodePort and redirect it to the appropriate backend Pods. This means the load balancing logic lives on each node, not in a central component.
The next logical step is understanding how to manage traffic routing for multiple services and external access, which leads to Kubernetes Ingress.