flowchart LR U["External client"] --> LB["LoadBalancer<br/>203.0.113.40:80"] LB --> NP["NodePort :31000<br/>on every node"] NP --> CIP["ClusterIP :80"] CIP --> P["pod :8080"]
Each type includes the previous one: LoadBalancer ⊃ NodePort ⊃ ClusterIP.
| Type | Reached at | Notes |
|---|---|---|
ClusterIP |
Inside the cluster only | Default |
NodePort |
<NodeIP>:<nodePort> from outside |
Port in the 30000–32767 range, opened on every node; calling a node without a pod still works (one extra hop) |
LoadBalancer |
External IP/hostname from the cloud | On self-built clusters/minikube, EXTERNAL-IP stays <pending> without MetalLB or minikube tunnel |
ExternalName |
A DNS name in the cluster | Only creates a CNAME record to externalName; no proxy, no endpoints |
spec:
type: NodePort
selector: { app: web }
ports:
- { port: 80, targetPort: 8080, nodePort: 31000 }
Common traps:
nodePort: 8080is rejected because it is outside the allowed range.- In the cloud, a NodePort also needs the firewall/security group opened for that port.
- ExternalName does no load balancing and no port mapping; clients still use the outside service port.
| Command | Meaning |
|---|---|
kubectl expose deployment web --type=NodePort --port=80 --target-port=8080 --name=web-np |
Create a NodePort |
kubectl get svc web-np |
PORT(S) column like 80:31000/TCP |
kubectl create service externalname ext-db --external-name db.example.com |
DNS alias to the outside |
LoadBalancer in the cloud
On EKS, AKS, or GKE, creating a LoadBalancer Service makes the cloud controller provision a real load balancer. Annotations choose options such as internal versus internet-facing:
| Cloud | Example annotation for an internal load balancer |
|---|---|
| AWS (Load Balancer Controller) | service.beta.kubernetes.io/aws-load-balancer-scheme: internal |
| Azure | service.beta.kubernetes.io/azure-load-balancer-internal: "true" |
| GCP | networking.gke.io/load-balancer-type: "Internal" |
Each LoadBalancer Service costs money and uses a public IP, so for many HTTP apps one Ingress or Gateway in front is cheaper.
Bare-metal clusters
Without a cloud provider, MetalLB or the load balancer built into k3s (ServiceLB) assigns external IPs from a pool you define and announces them on the LAN, filling EXTERNAL-IP so LoadBalancer Services work like in the cloud.
Preserving the client IP
By default, traffic arriving at a NodePort or LoadBalancer may be forwarded to a pod on another node, and the source IP is replaced. externalTrafficPolicy: Local sends traffic only to pods on the receiving node and keeps the real client IP, which matters for logging and IP allowlists; the load balancer's health checks then skip nodes without a pod.
Restricting who can connect
loadBalancerSourceRanges limits which client CIDRs may reach a LoadBalancer Service, for example an office range, and the cloud provider turns it into firewall rules.
Choosing a type
| Need | Choose |
|---|---|
| Internal calls between services | ClusterIP |
| Quick test from your laptop | kubectl port-forward |
| Simple external access in a lab | NodePort |
| Production TCP/UDP service (database proxy, MQTT, game server) | LoadBalancer |
| Many HTTP/HTTPS apps behind one entry point | Ingress or Gateway API in front of ClusterIP Services |
| Alias to an outside DNS name | ExternalName |
Whatever the type, the Service selects pods the same way; only how traffic reaches the ClusterIP changes.