Skip to content
Blog

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

TypeReachable fromExternal IPTypical use
ClusterIPIn-cluster onlyVirtual (internal)Internal services (default)
NodePortEverywhere (via node IPs)Node IP + high portDev/bare-metal, LB in front
LoadBalancerPublic internetCloud LB IPPublic services on cloud
ExternalNameIn-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: port is what clients connect to; targetPort is 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: None skipping 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.