How Different Types of Kubernetes Services Route Traffic to Pods
You may already know the major Service types in Kubernetes, and the main differences like “ClusterIP only has cluster-internal access”, “NodePort opens a port on every node” and “LoadBalancer is built on top of the former two types”.
But you may have questions such as what cluster-internal access “really” means; how routing happens all the way to a Pod, and so on. Let’s take a deeper look at what’s under the hood.
ClusterIP
ClusterIP is the default and most basic Service type. It exposes the Service on a cluster-internal IP address that is only reachable from within the cluster. This means:
- A stable virtual IP (VIP) is assigned to the Service
- The Service is only accessible within the Kubernetes cluster
- Pods in the cluster can access the Service using the cluster IP or DNS name
- External traffic cannot directly reach the Service
Under the hood:
- When a ClusterIP service is created, kube-proxy creates iptables rules
- These rules redirect traffic destined for the ClusterIP to the actual pod IPs
- Example iptables rules:
When you create a ClusterIP service, Kubernetes assigns a virtual IP to the Service, acting as a front of the actual backend pods. The kube-proxy watches the API for Services and Endpoints, then sets up a DNAT (Destination Network Address Translation) rule in the iptables nat table on all nodes, specifically in the KUBE-SERVICES chain.
In the cluster, if an application from a pod attempts to access the ClusterIP address, the packet goes through the iptables of that node. NAT translates it to the local or remote Pod IP transparently. This is why you can only “access pods within the cluster” for ClusterIP Service because it depends on the iptables on the node.
Setting Up a Test Cluster
We are using iptables as the default kube-proxy mode here. Deep dives to other modes, such as IPVS, are TBD.
Setup an EKS cluster using eksctl:
1 | Create an EKS cluster |
What happens when your applications within the cluster access the ClusterIP Service
Create a file nginx-deployment.yaml. We use pod affinity spec to create pods in different nodes.
1 | apiVersion: v1 |
And a service-clusterip.yaml
1 | apiVersion: v1 |
Deploy the yaml file and examine the pod status:
1 | Create the Deployment |
Now let’s check the iptables in a node. Note, we have 6 pods and 10 nodes. I am intentionally going to a node where no nginx pod is deployed, such as ip-192-168-40-246.us-west-2.compute.internal, so that we will see how a request can be routed to the destination Service.
We can use sudo iptables -t nat -L -n -v to list everything the NAT table. There’s a KUBE-SERVICES chain that is created by kube-proxy. Let’s take a look at it.
1 | sh-5.2$ sudo iptables -t nat -L KUBE-SERVICES -n -v --line-numbers | column -t |
We can see that there is a target KUBE-SVC-SKUSM527VLBNHFCG pointing to our newly created service routing/routing-service, with the ClusterIP address 10.100.5.54.
Let’s further check the specific chain KUBE-SVC-SKUSM527VLBNHFCG
1 | sh-5.2$ sudo iptables -t nat -L KUBE-SVC-SKUSM527VLBNHFCG -n -v --line-numbers | column -t |
We’ll see endpoint chains like KUBE-SEP-* representing the pods/endpoints. Then let’s see the specific pod/endpoint chain:
1 | sh-5.2$ sudo iptables -t nat -L KUBE-SEP-HSURKP7XCHIWJWR7 -n -v --line-numbers | column -t |
Here, we will see the DNAT rule to the Pod IP 192.168.11.88. Similarly, the other endpoint chains starting with KUBE-SEP-* would also have DNAT of their associated Pod IPs.
NodePort
NodePort builds on top of ClusterIP by exposing the Service on each Node’s IP at a static port (the NodePort). This means:
- The Service is accessible from outside the cluster using
<NodeIP>:<NodePort> - A port is allocated in the range 30000-32767 (configurable)
- The Service is still accessible within the cluster using ClusterIP
- Traffic can reach the Service through any Node’s IP address
Under the hood:
- kube-proxy creates additional iptables rules for the NodePort
- External traffic hitting any Node’s IP on the NodePort is forwarded to the Service’s ClusterIP
- The traffic is then distributed to pods using the existing ClusterIP rules
What happens when your applications within the cluster access the NodePort Service
Create another service service-nodeport.yaml.
1 | apiVersion: v1 |
We can see that the NodePort service has both ClusterIP and listening to an assigned port 30502 here.
1 | kubectl apply -f service-nodeport.yaml |
Since we have created another Service service-nodeport.yaml, we should see the new service under KUBE-SERVICES. There is a KUBE-NODEPORTS chain as the last target in the KUBE-SERVICES chain as we saw in above results. Let’s list the rules in KUBE-NODEPORTS chain.
1 | sh-5.2$ sudo iptables -t nat -L KUBE-NODEPORTS -n | column -t |
Let’s follow the KUBE-SVC-GA6B6UKIFOFCSJYD chain to further examine our service.
1 | sh-5.2$ sudo iptables -t nat -L KUBE-SVC-GA6B6UKIFOFCSJYD -n -v --line-numbers | column -t |
We can see that the KUBE-SEP-* chains under the KUBE-SVC- entry map to their corresponding pod IPs. Similar to what we have seen in ClusterIP above.
For packets destined to port 30502, originating either within or outside the cluster:
- the
KUBE-MARK-MASQrule marks the packet to be altered later in thePOSTROUTINGchain of the iptables, to use SNAT (source network address translation) to rewrite the source IP as the node IP - so that other hosts outside the pod network can reply back. - the DNAT (Destination Network Address Translation) target will rewrite the destination to a pod IP - this is how the request is routed to an actual pod
It is worth calling out that even if you send a request to a specific <NodeIp>:<Port>, the final pod destination may not be in the specified instance. This is because of the DNAT process according to the entries in the iptables.
LoadBalancer
LoadBalancer builds on top of NodePort by exposing the Service externally through a cloud provider’s load balancer. This means:
- The Service is accessible through a cloud provider’s load balancer IP
- The load balancer distributes traffic across all nodes
- The Service is still accessible through NodePort and ClusterIP
- Automatically provisions and configures the cloud load balancer
Under the hood:
- The cloud provider’s controller creates a load balancer
- The load balancer forwards traffic to NodePort
- NodePort forwards to ClusterIP
- ClusterIP distributes to pods
1 | apiVersion: v1 |
We can see that the LoadBalancer service has a ClusterIP, listening to a port, and an external IP. Since this is a bare minimum cluster in AWS, the EXTERNAL-IP is created as a classic AWS Load Balancer. You can go to AWS console -> EC2 -> Load Balancer to find that load balancer. The targets behind the load balancer are the 10 instances listening to port 32697. Requests will be routed by <instance-id>:32697 which is how NodePort works.
1 | kubectl apply -f service-loadbalancer.yaml |
If you use AWS Load Balancer Controller, Network Load Balancer (NLB) will be created for Service types. AWS Load Balancer Controller provides the flexibility to choose between instance or ip as target type (this flexibility also applies to Application Load Balancers for Ingress kinds).
- For instance type, AWS NLB registers instances/nodes as targets and routes in NodePort way
- For ip type, AWS NLB registers pod IPs as targets. It does not use NodePort under the hood. Instead, a request is directly routed to a pod, based on the routing algorithm configured in the NLB.
Note: LoadBalancer service type requires cloud provider integration. For bare metal or on-premises clusters, you’ll need additional solutions like MetalLB to provide load balancer functionality.
Service Types Comparison
| Feature | ClusterIP | NodePort | LoadBalancer |
|---|---|---|---|
| Accessibility | Internal cluster only | Internal + External via NodeIP:NodePort | Internal + External via Load Balancer IP |
| IP Assignment | Virtual ClusterIP only | ClusterIP + NodePort on all nodes | ClusterIP + NodePort + External LB IP |
| Port Range | Any port | 30000-32767 (default) | Any port (LB) + NodePort |
| Use Case | Internal microservices | Development/testing, direct node access | Production external services |
| Load Balancing | kube-proxy (iptables/IPVS) | kube-proxy + external routing | Cloud LB + kube-proxy |
| Cloud Provider Required | No | No | Yes (or MetalLB for bare metal) |
| Routing Flow | Client → ClusterIP → Pod | Client → NodeIP:Port → ClusterIP → Pod | Client → LB → NodePort → ClusterIP → Pod |
| iptables Rules | KUBE-SERVICES → KUBE-SVC-* → KUBE-SEP-* (DNAT to Pod) | KUBE-NODEPORTS → KUBE-EXT-* → KUBE-SVC-* → KUBE-SEP-* (MASQ + DNAT) | Same as NodePort (LB forwards to NodePort) |
Key Takeaways:
- ClusterIP provides internal service discovery and load balancing within the cluster using iptables DNAT rules
- NodePort builds on ClusterIP by adding external accessibility through a static port on every node
- LoadBalancer builds on NodePort by provisioning a cloud load balancer that distributes traffic across nodes
- All three types ultimately rely on kube-proxy’s iptables rules to route traffic to the actual Pods
- Traffic can land on any node and be routed to any Pod, regardless of Pod location
See Also
- https://kubernetes.io/docs/concepts/services-networking/service/
- https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/
- https://dustinspecker.com/posts/iptables-how-kubernetes-services-direct-traffic-to-pods/
- https://dustinspecker.com/posts/resolving-kubernetes-services-from-host-when-using-kind/
- https://dustinspecker.com/series/container-networking/
- https://ronaknathani.com/blog/2020/07/kubernetes-nodeport-and-iptables-rules/
- https://stackoverflow.com/questions/77034250/how-does-a-service-choose-which-pod-to-send-request-to
- https://www.reddit.com/r/kubernetes/comments/16a2us4/how_does_a_service_choose_which_pod_to_send/
- https://www.youtube.com/watch?v=uGm_A9qRCsk
- https://medium.com/@amroessameldin/kube-proxy-what-is-it-and-how-it-works-6def85d9bc8f