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 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 componentsContinue learning
Go deeper on infrastructure and delivery
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
The DevOps Handbook
Gene Kim, Jez Humble · 2021
Case studies from Etsy, Amazon, and Target on rewiring deployment pipelines and org structure.
Kubernetes: Up and Running
Brendan Burns, Joe Beda · 2022
Builds from a single Pod to multi-cluster deployments with YAML you type, not diagrams you skim.
4.6 (118)#10 in Web ServicesLearning Domain-Driven Design
Vlad Khononov · 2021
Turns bounded contexts and aggregates into decision rules for where service boundaries actually go.
4.6 (400)Designing Data-Intensive Applications
Martin Kleppmann, Chris Riccomini · 2026
Traces replication, consistency and consensus end to end, with the failure modes behind each choice.
3.6 (196)#1 in Data Mining