Kubernetes Service Types: ClusterIP vs NodePort vs LoadBalancer
Kubernetes offers four Service types for exposing pods on the network. How ClusterIP, NodePort, LoadBalancer, and ExternalName each route traffic.
A Kubernetes Service gives a stable network identity to a set of pods whose individual IP addresses change constantly as they’re rescheduled, scaled, or replaced. The type field on a Service determines where that stable identity is reachable from — inside the cluster only, on every node’s IP, or from the public internet — and picking the wrong one is one of the more common sources of “I can’t reach my app” confusion in a new cluster.
ClusterIP: the default, internal-only
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
type: ClusterIP
selector:
app: backend
ports:
- port: 80
targetPort: 8080
ClusterIP is the default type if you don’t specify one. It allocates a stable virtual IP address that’s only routable within the cluster, and registers a DNS name (backend.namespace.svc.cluster.local, or just backend from within the same namespace) that resolves to it. Any pod in the cluster can reach this service by name; nothing outside the cluster can reach it at all.
This is the right default for internal service-to-service traffic — a frontend calling a backend API, a backend calling a database proxy — which is the majority of traffic inside a typical microservices deployment. It pairs naturally with Kubernetes network policies, which can further restrict which pods are allowed to reach a given ClusterIP service, even though every pod can technically resolve its DNS name.
NodePort: exposed on every node’s IP
spec:
type: NodePort
selector:
app: backend
ports:
- port: 80
targetPort: 8080
nodePort: 30080
NodePort builds on ClusterIP — it still gets the internal virtual IP — but additionally opens a specific port (from a default range, typically 30000–32767) on every node in the cluster, regardless of whether that node is actually running a pod for the service. Traffic hitting <any-node-IP>:30080 gets routed to the service, which then load-balances it to a healthy pod, possibly on a different node.
NodePort is mostly a building block rather than a production-facing choice on its own: it’s how LoadBalancer type services are implemented under the hood in most cluster setups, and it’s occasionally used directly for quick testing or in bare-metal clusters without a cloud load balancer integration available. Exposing a NodePort directly to the internet means every node’s firewall has to allow that port, and clients need to know node IPs directly — brittle compared to the alternatives below.
LoadBalancer: the cloud-provisioned front door
spec:
type: LoadBalancer
selector:
app: backend
ports:
- port: 80
targetPort: 8080
LoadBalancer is what most production internet-facing services actually use. It builds on NodePort, but additionally asks the cloud provider the cluster is running on to provision an actual external load balancer — an AWS Network Load Balancer, a GCP Load Balancer, an Azure Load Balancer — with a real public IP address that routes to the NodePort on the cluster’s nodes.
This is the type that requires the most external infrastructure to actually work: it depends on the cluster having a cloud-controller-manager integration that knows how to talk to the underlying cloud provider’s API. A LoadBalancer service created in a bare-metal or local cluster without that integration will typically sit indefinitely with its external IP showing <pending>, because nothing exists to fulfill the request — a common point of confusion for anyone testing manifests locally before deploying to a managed cluster.
Because provisioning a cloud load balancer per service gets expensive at scale, most production clusters exposing many services externally use a single LoadBalancer service in front of an Ingress or Gateway API controller, which then does host- and path-based routing to many internal ClusterIP services behind it — one external IP and one load balancer bill, instead of one per service.
ExternalName: a DNS alias, no proxying at all
A fourth, less commonly used type, ExternalName, doesn’t route traffic through the cluster’s networking at all. It maps a Service name to an external DNS name via a CNAME record:
spec:
type: ExternalName
externalName: legacy-db.example.com
This is useful for giving an external dependency — a managed database, a third-party API — a name that fits the cluster’s internal DNS conventions, so application code can reference it the same way it references an in-cluster service, without hardcoding an external hostname. No proxying, load balancing, or health checking happens; it’s purely a DNS-level redirect.
Choosing the right type
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP | Inside the cluster only | Internal service-to-service traffic (the default, and most common) |
NodePort | Any node’s IP, on a specific port | A building block for LoadBalancer; rare as a direct choice |
LoadBalancer | The public internet, via a cloud LB | A small number of true entry points, often one in front of an Ingress |
ExternalName | N/A — DNS alias only | Referencing an external dependency by an internal-style name |
The general pattern in a well-structured cluster: most services stay ClusterIP, a single LoadBalancer service (or a couple, for distinct concerns like public API traffic vs internal admin tooling) sits at the actual edge, and an Ingress or Gateway API resource handles the routing fan-out behind it. This mirrors the layering you’d expect from any reverse proxy sitting in front of internal services — one well-known entry point, internal addresses that don’t need to be publicly routable.
The takeaway
Kubernetes Service types control where a set of pods becomes reachable: ClusterIP for internal-only traffic, NodePort as the building block that exposes a port on every node, LoadBalancer for a cloud-provisioned public entry point, and ExternalName for aliasing an external dependency into the cluster’s DNS. Most services in a real deployment should be ClusterIP, with a small number of LoadBalancer services — often just one, in front of an Ingress — handling the actual traffic from outside the cluster.
Tagged
Keep reading
Chisato · · 4 min read Kubernetes Init Containers Explained
Init containers run to completion before a pod's main containers start, making them the standard way to handle setup steps and startup ordering.
Chisato · · 4 min read Helm vs Kustomize for Kubernetes Config
Helm templates and packages Kubernetes manifests with a templating language; Kustomize patches plain YAML declaratively, with no templates at all.
The Lycoris Team · · 5 min read What Are Kubernetes CRDs? Custom Resources Explained
A Kubernetes CRD (CustomResourceDefinition) extends the API with new resource types, letting the cluster manage custom objects like native ones.