Service Types: Deep Dive
This document explains how each Kubernetes Service type routes traffic, why kube-proxy modes matter for performance, and when to choose between Service types, Ingress, and Gateway API. It connects the demo manifests to the networking data plane that makes services work.
What a Service Does
Section titled “What a Service Does”A Service provides a stable network endpoint for a set of pods. Pods come and go. Their IP addresses change on every restart. A Service gives you a DNS name and (usually) a virtual IP that stays constant regardless of which pods are backing it.
The demo creates five services, one of each type, all targeting the same backend:
apiVersion: apps/v1kind: Deploymentmetadata: name: echo-server namespace: service-types-demospec: replicas: 2 selector: matchLabels: app: echo-server template: metadata: labels: app: echo-server spec: containers: - name: echo image: registry.k8s.io/echoserver:1.10 ports: - containerPort: 8080Two pods, each listening on port 8080. Every Service type routes traffic to these pods differently.
ClusterIP: The Default
Section titled “ClusterIP: The Default”apiVersion: v1kind: Servicemetadata: name: echo-clusterip namespace: service-types-demospec: type: ClusterIP selector: app: echo-server ports: - port: 80 targetPort: 8080ClusterIP assigns a virtual IP from the cluster’s service CIDR (e.g., 10.96.0.0/12). This
IP is only routable within the cluster. External clients cannot reach it.
Traffic flow:
Pod A --> ClusterIP (10.96.X.X:80) --> kube-proxy rules --> Pod B (10.244.Y.Y:8080)The port: 80 is what clients connect to. The targetPort: 8080 is the port on the backend
pod. kube-proxy translates between them.
When to Use
Section titled “When to Use”ClusterIP is the right choice for internal service-to-service communication. A frontend pod
calls http://echo-clusterip:80 and kube-proxy load-balances the request to one of the backend
pods.
NodePort: External Access Without a Load Balancer
Section titled “NodePort: External Access Without a Load Balancer”apiVersion: v1kind: Servicemetadata: name: echo-nodeport namespace: service-types-demospec: type: NodePort selector: app: echo-server ports: - port: 80 targetPort: 8080 nodePort: 30080NodePort opens a port on every node in the cluster. Any traffic to <NodeIP>:30080 is
forwarded to the Service’s backend pods.
Traffic flow:
Client --> Node:30080 --> kube-proxy rules --> Pod (10.244.Y.Y:8080)NodePort ranges default to 30000-32767. You can specify a port within this range or let Kubernetes assign one.
A NodePort Service also gets a ClusterIP. It is a superset of ClusterIP. Internal pods can still use the ClusterIP. External clients use the NodePort.
When to Use
Section titled “When to Use”NodePort works for development, testing, and on-premise environments without cloud load balancers. In production cloud environments, prefer LoadBalancer or Ingress.
LoadBalancer: Cloud-Native External Access
Section titled “LoadBalancer: Cloud-Native External Access”apiVersion: v1kind: Servicemetadata: name: echo-loadbalancer namespace: service-types-demospec: type: LoadBalancer selector: app: echo-server ports: - port: 80 targetPort: 8080LoadBalancer provisions an external load balancer through the cloud provider’s controller (AWS ELB, GCP Network LB, Azure LB). The load balancer gets a public IP and forwards traffic to the NodePorts on each node.
Traffic flow:
Client --> Cloud LB (public IP:80) --> Node:NodePort --> kube-proxy --> PodA LoadBalancer Service includes both a ClusterIP and a NodePort. It is a superset of both.
On minikube, LoadBalancer services stay Pending unless you run minikube tunnel, which
simulates a cloud load balancer by assigning a local IP.
Cost Considerations
Section titled “Cost Considerations”Each LoadBalancer Service provisions a separate cloud load balancer. At $15-20/month per LB (typical cloud pricing), 20 services cost $300-400/month. This is why Ingress and Gateway API exist: they share a single load balancer across multiple services.
Headless Service: Direct Pod Access
Section titled “Headless Service: Direct Pod Access”apiVersion: v1kind: Servicemetadata: name: echo-headless namespace: service-types-demospec: clusterIP: None selector: app: echo-server ports: - port: 8080 targetPort: 8080Setting clusterIP: None creates a headless Service. No virtual IP is assigned. No
kube-proxy rules are created. DNS resolves directly to pod IPs.
Traffic flow:
Pod A --> DNS lookup (echo-headless) --> Pod IP directly --> Pod BDNS Behavior
Section titled “DNS Behavior”A normal ClusterIP Service returns a single A record pointing to the virtual IP:
echo-clusterip.service-types-demo.svc.cluster.local --> 10.96.42.1A headless Service returns multiple A records, one per pod:
echo-headless.service-types-demo.svc.cluster.local --> 10.244.0.5 --> 10.244.0.6The client receives all pod IPs and can implement its own load balancing or connect to a specific pod.
SRV Records
Section titled “SRV Records”Headless services also create SRV records that include port information:
_http._tcp.echo-headless.service-types-demo.svc.cluster.local --> 0 0 8080 10-244-0-5.echo-headless.service-types-demo.svc.cluster.local --> 0 0 8080 10-244-0-6.echo-headless.service-types-demo.svc.cluster.localSRV records are used by service discovery libraries that need to know both the address and the port.
When to Use
Section titled “When to Use”Headless services are required for StatefulSets (stable per-pod DNS). They are also useful when the client needs to know all backend pod IPs, like a database client that maintains connections to specific replicas.
ExternalName: DNS Alias
Section titled “ExternalName: DNS Alias”apiVersion: v1kind: Servicemetadata: name: external-api namespace: service-types-demospec: type: ExternalName externalName: httpbin.orgAn ExternalName Service creates a CNAME record in cluster DNS. No proxy, no ClusterIP, no endpoints. It is purely a DNS alias.
external-api.service-types-demo.svc.cluster.local --> CNAME --> httpbin.orgBehavior and Limitations
Section titled “Behavior and Limitations”- No port remapping. The client must use the target service’s port.
- No health checking. The DNS record exists even if the target is down.
- HTTPS clients must handle the hostname mismatch. The TLS certificate for
httpbin.orgwill not matchexternal-api. - Some client libraries do not follow CNAME records correctly, especially with HTTP/2.
When to Use
Section titled “When to Use”ExternalName services let you reference external services by an internal DNS name. If you later move the service into the cluster, you change the Service type from ExternalName to ClusterIP without updating any client code.
kube-proxy Modes
Section titled “kube-proxy Modes”kube-proxy implements the Service abstraction by programming networking rules on every node. It runs as a DaemonSet and watches the API server for Service and EndpointSlice changes.
iptables Mode (Default)
Section titled “iptables Mode (Default)”kube-proxy creates iptables rules in the nat table. For each Service, there is a chain that
DNAT’s (Destination NAT) the virtual IP to a pod IP. Load balancing is done probabilistically
using iptables --probability matches.
Advantages:
- Mature and well-tested.
- No user-space proxying; packets stay in kernel space.
Disadvantages:
- O(n) rule evaluation. With thousands of services, iptables performance degrades.
- Adding or removing a service requires updating the entire rule set.
- Load balancing is random, not round-robin or least-connections.
IPVS Mode
Section titled “IPVS Mode”IPVS (IP Virtual Server) uses kernel-level load balancing hash tables. It supports O(1) connection dispatch regardless of the number of services.
Advantages:
- Better performance at scale (10,000+ services).
- Multiple load balancing algorithms: round-robin, least-connections, source-hash.
- Incremental rule updates (add/remove individual services).
Disadvantages:
- Requires the
ip_vskernel module. - Some edge cases with iptables interaction.
nftables Mode (Kubernetes 1.29+)
Section titled “nftables Mode (Kubernetes 1.29+)”nftables is the successor to iptables. The nftables kube-proxy mode translates Service rules to nftables syntax. It offers better performance than iptables and uses verdicts and maps instead of long chains.
This is the newest mode and is considered the future direction. It provides the same semantics as iptables mode but with better rule management.
Which Mode to Choose
Section titled “Which Mode to Choose”| Scenario | Recommended Mode |
|---|---|
| Small clusters (< 1000 services) | iptables (default) |
| Large clusters (1000+ services) | IPVS |
| New installations (1.29+) | nftables |
| Edge cases with iptables | IPVS |
EndpointSlices vs Endpoints
Section titled “EndpointSlices vs Endpoints”Endpoints (Legacy)
Section titled “Endpoints (Legacy)”An Endpoints object lists all pod IPs for a Service in a single object. For a Service with 1000 pods, the Endpoints object contains 1000 addresses. Every time a pod changes, the entire object is updated and sent to every watching node.
EndpointSlices (Kubernetes 1.21+ default)
Section titled “EndpointSlices (Kubernetes 1.21+ default)”EndpointSlices split endpoints into smaller groups (default 100 per slice). A Service with 1000 pods has 10 EndpointSlice objects. When one pod changes, only the affected slice is updated.
Benefits:
- Smaller watch payloads. Only changed slices are transmitted.
- Better for large services. Endpoints objects have a practical limit around 1000 addresses.
- Support for dual-stack (IPv4 and IPv6) and topology hints.
kube-proxy watches EndpointSlices by default.
Session Affinity
Section titled “Session Affinity”By default, kube-proxy distributes requests randomly (iptables mode) or round-robin (IPVS mode). Session affinity routes all requests from the same client to the same pod.
spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800ClientIP affinity uses the client’s source IP to determine which pod receives the request.
All requests from the same IP go to the same pod for timeoutSeconds (default 10800, or 3
hours).
Limitations:
- Only works with
ClientIP. There is no cookie-based affinity at the Service level. Use Ingress for that. - Does not work well with NAT or shared source IPs, where many clients appear as one IP.
External Traffic Policy
Section titled “External Traffic Policy”The externalTrafficPolicy field controls how NodePort and LoadBalancer Services handle
traffic from outside the cluster.
Cluster (Default)
Section titled “Cluster (Default)”Traffic can be routed to pods on any node. If a request arrives at Node A but the pod is on Node B, kube-proxy forwards it across nodes.
spec: externalTrafficPolicy: ClusterAdvantage: Even load distribution. Disadvantage: Extra network hop. The source IP is lost (SNAT).
Traffic is only routed to pods on the node that received it. If no pod runs on that node, the request is dropped (or the load balancer health check removes that node).
spec: externalTrafficPolicy: LocalAdvantage: Preserves client source IP. No extra hop. Disadvantage: Uneven load distribution (nodes with more pods get more traffic).
When to Use Local
Section titled “When to Use Local”Use Local when:
- You need the client’s real IP address (logging, rate limiting, geo-routing).
- You want to minimize network latency (no cross-node forwarding).
- Your load balancer supports health checks (it removes nodes without pods).
Internal Traffic Policy
Section titled “Internal Traffic Policy”Similar to externalTrafficPolicy but for traffic originating inside the cluster.
spec: internalTrafficPolicy: LocalWith Local, in-cluster traffic is routed only to pods on the same node as the caller. This
reduces cross-node traffic but requires pods to be present on every node (or accept failures
when no local pod exists).
This is useful for node-local services like log aggregators or caches.
Multi-Port Services
Section titled “Multi-Port Services”A single Service can expose multiple ports:
spec: ports: - name: http port: 80 targetPort: 8080 - name: grpc port: 9090 targetPort: 9090When multiple ports are defined, each port must have a name. The name is used in
EndpointSlices and by Ingress rules.
Ingress vs Gateway API
Section titled “Ingress vs Gateway API”Ingress
Section titled “Ingress”Ingress is a Kubernetes resource for HTTP routing. It sits in front of ClusterIP Services and routes by host and path. An Ingress Controller (nginx, HAProxy, Traefik, Envoy) implements the spec. It typically runs as a Deployment with a LoadBalancer Service, sharing that single load balancer across all Ingress rules.
Advantages: widely supported, single load balancer for multiple services, HTTP-level features like TLS termination and path-based routing.
Limitations: HTTP/HTTPS only, limited extensibility (annotations are vendor-specific), no built-in traffic splitting.
Gateway API
Section titled “Gateway API”Gateway API is the successor to Ingress. It separates infrastructure concerns (Gateway) from application routing (HTTPRoute). It supports HTTP, HTTPS, TCP, UDP, gRPC, and TLS. It has built-in traffic splitting for canary deployments.
Which to Choose
Section titled “Which to Choose”| Scenario | Recommendation |
|---|---|
| Simple HTTP routing | Ingress |
| TCP/UDP routing | Gateway API |
| Multi-team environments | Gateway API |
| Traffic splitting/canary | Gateway API |
| Maximum compatibility | Ingress |
| New projects (Kubernetes 1.27+) | Gateway API |
Service Topology and Topology-Aware Routing
Section titled “Service Topology and Topology-Aware Routing”Topology-aware routing (successor to the deprecated Service Topology feature) uses topology hints in EndpointSlices to prefer routing traffic to pods in the same zone.
When enabled, kube-proxy prefers endpoints in the same availability zone as the calling pod. This reduces cross-zone traffic and associated costs.
metadata: annotations: service.kubernetes.io/topology-mode: AutoIn Auto mode, the EndpointSlice controller adds topology hints based on zone distribution.
kube-proxy uses these hints to prefer local endpoints.
This works best when pods are evenly distributed across zones. With skewed distributions, some zones may not have enough capacity, causing overload.
How Traffic Flows: Complete Picture
Section titled “How Traffic Flows: Complete Picture”| Type | Path |
|---|---|
| ClusterIP | Pod -> CoreDNS -> ClusterIP -> kube-proxy DNAT -> Pod IP |
| NodePort | Client -> Node:30080 -> kube-proxy DNAT -> Pod IP |
| LoadBalancer | Client -> Cloud LB -> Node:NodePort -> kube-proxy DNAT -> Pod IP |
| Headless | Pod -> CoreDNS (returns pod IPs) -> direct connection to Pod IP |
| ExternalName | Pod -> CoreDNS (CNAME) -> external DNS -> external service |
For NodePort and LoadBalancer with externalTrafficPolicy: Cluster, kube-proxy may forward
across nodes to reach the target pod. With externalTrafficPolicy: Local, traffic stays on
the receiving node.
Connection to the Demo
Section titled “Connection to the Demo”The demo deploys all five Service types against the same backend. This lets you compare their behavior directly:
- ClusterIP: Only reachable from inside the cluster. Use port-forward for local access.
- NodePort: Reachable at
$(minikube ip):30080without port-forward. - LoadBalancer: Requires
minikube tunnelto simulate a cloud LB. - Headless: DNS returns pod IPs instead of a virtual IP.
- ExternalName: DNS alias to
httpbin.org. No local pods involved.
The backend is a 2-replica echoserver that returns request details (headers, source IP,
hostname). This makes it easy to see which pod handled the request and whether the client
IP was preserved.
Common Pitfalls
Section titled “Common Pitfalls”LoadBalancer Pending Forever
Section titled “LoadBalancer Pending Forever”On bare-metal or minikube, there is no cloud controller to provision a load balancer. The
Service stays Pending. Use MetalLB for bare-metal LoadBalancer support, or minikube tunnel
for local development.
NodePort Conflicts
Section titled “NodePort Conflicts”Two Services cannot use the same NodePort. If you hardcode nodePort: 30080, no other Service
can use port 30080. Let Kubernetes assign ports automatically to avoid conflicts.
ExternalName and TLS
Section titled “ExternalName and TLS”Connecting to an ExternalName Service over HTTPS will fail certificate validation. The
certificate is for httpbin.org, but the client connected to external-api. Either accept
the mismatch or use a proxy that rewrites the Host header.
Headless Service Without Selectors
Section titled “Headless Service Without Selectors”A headless Service without a selector creates no EndpointSlices. You must create Endpoints manually. This is useful for pointing at external hosts by IP.
Further Reading
Section titled “Further Reading”- Kubernetes Service documentation
- EndpointSlices
- kube-proxy modes
- Ingress
- Gateway API
- Topology Aware Routing