Kubernetes Services
TL;DR
- A Service is a stable virtual IP and DNS name in front of a set of Pods selected by label; it exists because Pod IPs are ephemeral and change on every restart/reschedule.
- Four types cover distinct exposure levels: ClusterIP (internal only, default), NodePort (host port 30000-32767 on every node), LoadBalancer (provisions a cloud LB, builds on NodePort/ClusterIP), ExternalName (DNS CNAME alias, no selector, no proxying).
- Services are not processes —
kube-proxyon every node programsiptables/IPVS rules in the kernel to DNAT traffic from the Service VIP to a Pod IP. - The single biggest gotcha: an empty
Endpoints/EndpointSlicelist almost always means the Serviceselectordoes not match the Pod’slabels(frequently a case mismatch) — the Service object itself will look healthy. - Headless Services (
clusterIP: None) skip the VIP entirely and return individual Pod A records via DNS — required for peer-aware stateful workloads.
Core Concepts
Why Services Exist
Pod IP addresses are dynamic: Pods are recreated on rescheduling, scaling, or rollout, and each new Pod gets a new IP. Clients cannot reliably target individual Pod IPs. A Service provides a stable virtual IP (VIP) and DNS name that persists independent of which Pods are currently backing it.
Service Types
ClusterIP (default). Allocates a stable, internal-only IP from the cluster’s Service CIDR. Only reachable from inside the cluster. Used for internal microservices — databases, backend APIs, caches.
NodePort. Exposes the Service on every Node’s IP at a static port in the 30000-32767 range. A client connects to :; kube-proxy intercepts the traffic on that node and routes it to a backing Pod, which may live on a different node. Used for local/dev access or bridging to external load balancers that can’t provision native cloud LBs.
LoadBalancer. Provisions a cloud provider’s native load balancer (AWS NLB/ALB, GCP Cloud Load Balancer). Under the hood it automatically creates a NodePort and ClusterIP, then configures the cloud LB to forward to the NodePort on cluster nodes. Used for public-facing applications.
ExternalName. Maps the Service name to an external DNS name via a CNAME record. Has no selector and does no proxying/load balancing — it is purely a DNS-level redirect. Used to give pods a stable internal name for an external dependency (e.g., an RDS endpoint).
Labels, Selectors, and EndpointSlices
Services decouple clients from specific Pods using label selectors. When a Service is created, the control plane continuously computes an EndpointSlice object that maps the Service to the actual IPs of currently-matching, ready Pods. This mapping is what kube-proxy consumes to program routing rules — it is not a static snapshot.
Under the Hood: kube-proxy and Kernel-Level Routing
A Service is not a running proxy process (unlike an Nginx instance). It is a logical spec realized by cluster components:
- VIP assignment. The API Server assigns a virtual IP to the Service from the cluster’s Service CIDR range.
- kube-proxy sync.
kube-proxy, running as a daemon on every Node, watches the API server for Service and EndpointSlice changes. - Kernel programming.
kube-proxywritesiptablesrules (or IPVS rules, in IPVS mode) directly into the host Node’s kernel netfilter tables. - Packet routing. When a container sends a packet to the Service VIP, the host kernel intercepts it via these rules, selects a backing Pod (e.g., iptables’ random-probability matching to approximate load balancing), rewrites the destination IP to that Pod’s IP (DNAT), and forwards the packet. No user-space proxy is on the data path in iptables mode.
Headless Services
Setting clusterIP: None explicitly disables VIP allocation and proxying. CoreDNS then returns a list of A records — one per matching, ready Pod IP — instead of a single virtual IP. Clients performing a DNS lookup get direct Pod IPs and can connect peer-to-peer. This is required for stateful clustered systems (Cassandra, Elasticsearch, Kafka) where replicas must establish direct connections to specific peers rather than being load-balanced round-robin.
DNS Resolution
Every Service gets a DNS entry via CoreDNS. Within the same namespace, the short name (db-service) resolves. Across namespaces, the FQDN is required: ..svc.cluster.local (e.g., db-service.data.svc.cluster.local).
ClusterIP vs NodePort vs LoadBalancer vs ExternalName
| Aspect | ClusterIP | NodePort | LoadBalancer | ExternalName |
|---|---|---|---|---|
| Reachable from | Inside cluster only | Any node IP, port 30000-32767 | Public internet (via cloud LB) | Inside cluster (DNS alias) |
| Builds on | — | ClusterIP | NodePort + ClusterIP | — |
| Uses selector | Yes | Yes | Yes | No |
| Requires cloud provider integration | No | No | Yes | No |
| Typical use | Internal services, DBs | Dev/test, legacy LB integration | Public-facing apps | Pointing to external endpoints |
Command / Configuration Reference
apiVersion: v1
kind: Service
metadata:
name: auth-service
namespace: default
spec:
type: ClusterIP
selector:
app: auth-api # Finds all pods with label "app: auth-api"
ports:
- name: http
port: 80 # Port exposed by the Service VIP
targetPort: 3000 # Port the container actually listens on
# List all Services in the current namespace
kubectl get svc
# Describe a Service to see its mapped endpoints (target Pod IPs)
kubectl describe svc auth-service
# Check the "Endpoints:" field — if empty, the selector doesn't match Pod labels
# Expose an existing Deployment imperatively as a NodePort Service
kubectl expose deployment auth-api --name=auth-service --type=NodePort --port=80 --target-port=3000
# Verify Service DNS resolution from inside a throwaway pod
kubectl run dns-diagnostic --rm -i --tty --image=busybox -- nslookup auth-service
Common Pitfalls
- Pitfall:
kubectl describe svcshows an emptyEndpointslist and traffic to the Service times out. Why: The Service’sselectordoes not match any Pod’slabels— often a case-sensitivity mismatch (app: frontendvsapp: FrontEnd); the Service object itself validates fine. Fix: Comparekubectl get svc -o yamlselector againstkubectl get pods --show-labelsand align them exactly. - Pitfall: Assuming a Service is a running proxy that can be restarted or debugged like a pod. Why: A Service is a logical object; the actual routing is kernel-level
iptables/IPVS rules maintained bykube-proxyon each node. Fix: Debug by checkingkube-proxylogs/rules on the node and the EndpointSlice object, not the Service resource itself. - Pitfall: Expecting a NodePort Service to load-balance evenly and immediately after a Pod is deleted. Why: Routing depends on
kube-proxyreconciling updated EndpointSlices, which is eventually consistent, not instantaneous. Fix: Account for brief propagation delay; use readiness probes so terminating/starting Pods aren’t sent traffic prematurely. - Pitfall: Using a regular Service for a StatefulSet where replicas need to address each other individually. Why: A normal Service load-balances across a single VIP, hiding individual Pod identity. Fix: Use a Headless Service (
clusterIP: None) so DNS returns per-Pod A records.
Interview Questions
- What is a Headless Service and when would you use one? — Tests understanding of
clusterIP: Noneand stateful workload requirements. - Walk through what happens at the kernel level when a Service receives a packet. — Tests depth of understanding of kube-proxy, iptables/IPVS, and DNAT.
- A Service has Pods running and healthy, but
kubectl describe svcshows no endpoints — how do you debug it? — Tests practical troubleshooting of selector/label mismatches. - Why does LoadBalancer type depend on the cloud provider, and what happens if you use it on bare-metal? — Tests knowledge of cloud controller integration and tools like MetalLB.
- How does DNS resolution differ for a Service in the same namespace versus a different namespace? — Tests FQDN and
svc.cluster.localknowledge.