Kubernetes Service Types — ClusterIP, NodePort, LoadBalancer, ExternalName, and Headless Services
Why Kubernetes needs Services#
Pods are ephemeral. They get new IP addresses every time they restart. A deployment with 3 replicas has 3 different IPs, and those IPs change on every rolling update. You cannot hardcode pod IPs.
Services provide a stable network identity for a set of pods. One DNS name, one virtual IP, load-balanced across all matching pods. Pods come and go; the Service stays.
ClusterIP — the default#
Every Service is a ClusterIP unless you say otherwise. It assigns a virtual IP that's only reachable from inside the cluster.
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: backend
spec:
type: ClusterIP
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
protocol: TCP
How it works#
- You create a Service with a label selector (
app: user-service) - Kubernetes assigns a virtual IP (e.g.,
10.96.45.12) - kube-proxy on every node programs iptables or IPVS rules to forward traffic from the virtual IP to the actual pod IPs
- DNS resolves
user-service.backend.svc.cluster.localto10.96.45.12
When to use#
- Service-to-service communication inside the cluster
- Backend APIs that frontends call through an ingress, not directly
- Databases, caches, and internal tools that should never be publicly exposed
Named ports#
ports:
- name: http
port: 80
targetPort: 8080
- name: grpc
port: 9090
targetPort: 9090
Named ports let you reference ports by name in other resources (NetworkPolicies, Ingress). They also decouple the Service port from the container port.
NodePort — expose on every node#
NodePort opens a specific port (30000-32767) on every node in the cluster. Traffic to any node's IP on that port gets forwarded to the Service.
apiVersion: v1
kind: Service
metadata:
name: web-app
spec:
type: NodePort
selector:
app: web-app
ports:
- port: 80
targetPort: 3000
nodePort: 31000
How it works#
- Kubernetes allocates a port from the 30000-32767 range (or you specify one)
- kube-proxy opens that port on every node
- Traffic to
any-node-ip:31000gets forwarded to pods matching the selector - A ClusterIP is also created — NodePort is a superset of ClusterIP
When to use#
- Development and testing environments
- When you have your own external load balancer that routes to node IPs
- On-premise clusters without cloud load balancer integration
When NOT to use#
- Production internet-facing services (use LoadBalancer or Ingress instead)
- The 30000-32767 range is non-standard and firewall-unfriendly
LoadBalancer — cloud-native ingress#
LoadBalancer provisions a cloud load balancer (AWS NLB/ALB, GCP LB, Azure LB) that routes external traffic to your Service.
apiVersion: v1
kind: Service
metadata:
name: api-gateway
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
type: LoadBalancer
selector:
app: api-gateway
ports:
- port: 443
targetPort: 8443
protocol: TCP
How it works#
- Kubernetes asks the cloud provider to provision a load balancer
- The cloud provider creates an external IP or DNS name
- Traffic flows: Internet -> Cloud LB -> NodePort -> Pod
- The
status.loadBalancer.ingressfield shows the external address
When to use#
- Production services that need external traffic
- TCP/UDP services that can't use HTTP Ingress (databases, gRPC, game servers)
- When you need cloud provider features (health checks, SSL termination, static IPs)
Cost consideration#
Every LoadBalancer Service provisions a separate cloud load balancer. At $15-25/month each on AWS, 20 services cost $300-500/month. Use an Ingress controller to share one load balancer across multiple HTTP services.
ExternalName — DNS alias#
ExternalName maps a Service name to an external DNS name. No proxying, no port forwarding. Just a CNAME record.
apiVersion: v1
kind: Service
metadata:
name: analytics-db
namespace: backend
spec:
type: ExternalName
externalName: analytics.us-east-1.rds.amazonaws.com
How it works#
- DNS lookup for
analytics-db.backend.svc.cluster.localreturns a CNAME toanalytics.us-east-1.rds.amazonaws.com - The application connects to the external service using the internal DNS name
- No ClusterIP is assigned, no kube-proxy rules are created
When to use#
- Referencing external databases (RDS, Cloud SQL) with a cluster-local name
- Migrating services out of the cluster without changing application config
- Abstracting external service endpoints behind a stable internal name
Limitations#
- No port remapping — the external service must use the same port
- No health checking — Kubernetes doesn't monitor the external endpoint
- HTTPS/TLS may break if the external service validates hostnames (SNI mismatch)
Headless Services — direct pod access#
A headless Service has clusterIP: None. DNS returns the individual pod IPs instead of a single virtual IP.
apiVersion: v1
kind: Service
metadata:
name: cassandra
spec:
clusterIP: None
selector:
app: cassandra
ports:
- port: 9042
targetPort: 9042
How it works#
- No ClusterIP is assigned
- DNS lookup for
cassandra.default.svc.cluster.localreturns A records for every pod:10.244.1.5,10.244.2.8,10.244.3.12 - The client decides which pod to connect to
When to use#
- StatefulSets — each pod gets a stable DNS name:
cassandra-0.cassandra.default.svc.cluster.local - Client-side load balancing — gRPC clients that manage their own connections
- Databases — Cassandra, MongoDB, Elasticsearch nodes that need peer discovery
- Service meshes — when the sidecar proxy handles load balancing
StatefulSet DNS#
With a headless Service named cassandra and a StatefulSet, each pod gets a predictable DNS record:
cassandra-0.cassandra.default.svc.cluster.local
cassandra-1.cassandra.default.svc.cluster.local
cassandra-2.cassandra.default.svc.cluster.local
These names survive pod restarts. The pod IP changes, but the DNS name stays.
Service discovery and DNS resolution#
CoreDNS#
Kubernetes runs CoreDNS as the cluster DNS server. Every pod's /etc/resolv.conf points to it.
# Pod's /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
ndots: 5
DNS record formats#
| Record | Format |
|---|---|
| Service A record | service.namespace.svc.cluster.local |
| Pod A record | pod-ip.namespace.pod.cluster.local |
| SRV record | _port._protocol.service.namespace.svc.cluster.local |
| StatefulSet pod | pod-name.service.namespace.svc.cluster.local |
Short names#
Thanks to the search domain, you can use short names:
# Within same namespace
curl http://user-service
# Cross-namespace
curl http://user-service.backend
# Fully qualified (no DNS search, fastest resolution)
curl http://user-service.backend.svc.cluster.local.
The trailing dot in the fully qualified name prevents DNS search iteration — it resolves in one query instead of up to five.
Endpoints and EndpointSlices#
Endpoints#
Every Service with a selector automatically creates an Endpoints object that lists the IP:port pairs of all matching, ready pods:
kubectl get endpoints user-service
# NAME ENDPOINTS AGE
# user-service 10.244.1.5:8080,10.244.2.8:8080 5d
EndpointSlices#
EndpointSlices replace Endpoints for large-scale clusters. Each slice holds up to 100 endpoints. For a Service with 1,000 pods, that's 10 slices instead of one massive Endpoints object.
kubectl get endpointslices -l kubernetes.io/service-name=user-service
Services without selectors#
You can create a Service without a selector and manually manage the endpoints:
apiVersion: v1
kind: Service
metadata:
name: external-api
spec:
ports:
- port: 443
targetPort: 443
---
apiVersion: v1
kind: Endpoints
metadata:
name: external-api
subsets:
- addresses:
- ip: 203.0.113.50
ports:
- port: 443
This is useful for routing cluster traffic to external IPs that aren't DNS names (where ExternalName won't work).
Choosing the right Service type#
| Scenario | Service type |
|---|---|
| Internal service-to-service | ClusterIP |
| Development/testing external access | NodePort |
| Production external traffic (TCP/UDP) | LoadBalancer |
| HTTP/HTTPS with path routing | ClusterIP + Ingress |
| External database or SaaS | ExternalName |
| StatefulSet peer discovery | Headless (clusterIP: None) |
| gRPC with client-side balancing | Headless (clusterIP: None) |
The practical takeaway#
Kubernetes Services abstract pod networking into stable, discoverable endpoints. The decision tree:
- ClusterIP is the default and correct choice for 80% of services — internal communication with DNS discovery
- LoadBalancer for production external traffic, but share one via Ingress for HTTP services to control costs
- Headless for StatefulSets and client-side load balancing where you need direct pod access
- ExternalName to alias external services behind cluster DNS names
- NodePort only for development or when you bring your own load balancer
- Always use fully qualified DNS names with trailing dots in latency-sensitive paths to avoid search domain overhead
Article #453 in the Codelit engineering series. Explore our full library of system design, infrastructure, and architecture guides at codelit.io.
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Cost Estimator
See estimated AWS monthly costs for every component in your architecture
GitHub Integration
Paste a repo URL and generate architecture from your actual codebase
Related articles
Try these templates
Scalable SaaS Application
Modern SaaS with microservices, event-driven processing, and multi-tenant architecture.
10 componentsURL Shortener Service
Scalable URL shortening with analytics, custom aliases, and expiration.
7 componentsHeadless CMS Platform
Headless content management with structured content, media pipeline, API-first delivery, and editorial workflows.
8 componentsBuild this architecture
Generate an interactive architecture for Kubernetes Service Types in seconds.
Try it in Codelit →
Comments