Kubernetes Service Types Explained: ClusterIP, NodePort, LoadBalancer, ExternalName
Pods are ephemeral and every one has its own IP — Services fix that. Understand the four Service types and pick the right one for every workload.
Published on • October 11, 2026
AI Assistant

Pods are created and destroyed constantly. Their IP addresses change with every rollout. If your frontend hardcodes backend IPs, you’re broken every deploy. The Kubernetes Service is the fix: a stable virtual endpoint in front of a changing set of pods — and the type field decides exactly how far that endpoint reaches.
Why Services exist
A Deployment recreates pods at will; each pod gets a fresh IP. Services provide:
- A stable virtual IP and DNS name (
my-svc.default.svc.cluster.local) that doesn’t change. - Load balancing across the pods matching a label selector.
- Decoupling — clients address the Service, never the pods.
A minimal Service:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
This creates the default type — ClusterIP — targeting TCP 9376 on every pod labeled app: MyApp. The controller continuously syncs EndpointSlices (the modern replacement for the deprecated Endpoints object) as pods come and go.
The four types
ClusterIP (default): cluster-internal only
spec:
type: ClusterIP # or omit entirely
Kubernetes assigns a virtual IP from a reserved cluster range. The Service is reachable only from inside the cluster — via the IP, via DNS, or from other pods. Nothing outside can reach it.
Use it for: internal microservice-to-microservice traffic — databases, backends, workers, internal APIs. This is the default you want ~90% of the time; reach the outside world with Ingress or Gateway API in front, not by changing type.
Special case: clusterIP: None creates a headless Service — no VIP, no load balancing, direct DNS resolution to individual pod IPs. Stateful workloads (databases needing stable identities, per-pod discovery) use this.
NodePort: expose on every node
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30007 # optional; auto-allocated from 30000-32767 otherwise
NodePort opens a static port (default range 30000–32767, allocated by the control plane) on every node’s IP. Connect to any-node-ip:30007 and traffic is proxied to your pods. It also creates a ClusterIP underneath — NodePort adds to it, the types are layered.
The port range is split into bands to avoid collisions: 30000–30085 is the static band (for manual assignment), 30086–32767 is dynamic.
Use it for: quick demos, bare-metal clusters where you run your own load balancer in front, or direct node access. Not for production ingress — no TLS, no routing rules, and you’re hardcoding node IPs. Put MetalLB or a proper gateway in front instead.
LoadBalancer: cloud-native external exposure
spec:
type: LoadBalancer
On a cloud provider (AWS, GCP, Azure), this provisions a real external load balancer asynchronously — the allocated IP appears in .status.loadBalancer.ingress. Under the hood Kubernetes first does the NodePort work, then the cloud controller points the LB at those node ports.
Use it for: public-facing services in the cloud when you want the simplest possible path to an external IP — and don’t mind paying per LB. On bare metal it stays Pending unless you install something like MetalLB that implements the contract.
Cost and sprawl warning: one LoadBalancer per service means one cloud LB per service. That’s where Ingress/Gateway (one LB, many routes) wins at scale.
ExternalName: a DNS alias, nothing more
spec:
type: ExternalName
externalName: api.example.com
No proxying, no IPs, no ports — Kubernetes configures CoreDNS to return a CNAME to that hostname. The service name becomes an in-cluster alias for an external dependency.
Use it for: giving a stable in-cluster name to SaaS or legacy endpoints (postgres.rds.amazonaws.com → my-db), so application config never changes across environments. A key trick: ExternalName Services have no selector, and any Service without a selector can manually bind to arbitrary endpoints via a hand-written EndpointSlice — useful for pointing at databases outside the cluster.
Choosing at a glance
| Type | Reachable from | External IP | Typical use |
|---|---|---|---|
ClusterIP | In-cluster only | Virtual (internal) | Internal services (default) |
NodePort | Everywhere (via node IPs) | Node IP + high port | Dev/bare-metal, LB in front |
LoadBalancer | Public internet | Cloud LB IP | Public services on cloud |
ExternalName | In-cluster (DNS only) | — | Alias to external dependency |
How they fit with Ingress and Gateway
Service types answer “how does this reach the network?” — Ingress and the Gateway API answer “how does HTTP traffic get routed to it?”. The common production shape:
- Backends:
ClusterIP(never exposed directly). - One
LoadBalancer(or NodePort on bare metal) fronting an ingress/gateway controller. - Host/path routing rules in Ingress/Gateway objects pointing at the ClusterIP services.
You get one external entry point, TLS termination, and path-based routing — without a cloud LB per app.
Gotchas
- Selector drift: if no pods match the selector, the Service resolves to nothing —
kubectl get endpoints(or EndpointSlices) shows why. - Ports vs targetPort:
portis what clients connect to;targetPortis the container’s port (can be a named container port, letting pods change ports without breaking clients). - Multi-port Services need names for every port.
- ExternalName needs no ports — it’s DNS-only.
- Headless ≠ broken:
clusterIP: Noneskipping the VIP is intentional for StatefulSets.
The full reference is the Kubernetes Service concept page. Start every workload as ClusterIP, and only widen exposure when traffic actually needs to leave the cluster.