ClusterIP Services
[!NOTE] This module explores the core principles of ClusterIP Services, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. The Problem: Ephemeral IPs
In the world of virtual machines, IP addresses were static. You could hardcode db-server: 192.168.1.50 into your application config.
In Kubernetes, Pods are mortal. They are created and destroyed dynamically by Deployments, DaemonSets, or scaling events. Each time a Pod is reborn, it gets a new IP address.
If a frontend Pod tries to talk to a backend Pod using its direct IP, the connection will break as soon as the backend Pod restarts. We need a stable abstraction.
2. The Solution: Service (ClusterIP)
A Service is an abstraction that defines a logical set of Pods and a policy by which to access them. The default type is ClusterIP.
- Stable Virtual IP: The Service is assigned a Virtual IP (VIP) from a special range (e.g.,
10.96.0.0/12). This IP never changes as long as the Service exists. - Stable DNS Name: Kubernetes automatically assigns a DNS name like
my-service.my-ns.svc.cluster.local. - Load Balancing: Traffic sent to the VIP is automatically load-balanced across the healthy backing Pods.
[!NOTE] The ClusterIP is a Virtual IP. It doesn’t exist on any network interface. It is a fake IP that is intercepted by
iptablesorIPVSrules on every node.
3. Interactive: Service Discovery Visualizer
Visualize how traffic is routed from a client to a dynamic set of backend Pods. Observe how the Service abstraction shields the client from Pod churn.
Frontend Pod
curl http://backend
Service (ClusterIP)
Backend Pods
4. Under the Hood: kube-proxy
How does a “fake” IP address like 10.96.0.1 actually work? The magic happens via kube-proxy, a component running on every node.
1. The Watcher
kube-proxy watches the Kubernetes API Server for changes to Services and Endpoints (Pods).
2. The Rule Maker
When a new Service is created, kube-proxy writes rules to the node’s packet filtering system.
- iptables mode (Legacy/Standard): Uses Linux iptables NAT tables. It’s O(N) complexity, meaning it gets slow with thousands of services.
- IPVS mode (Performance): Uses Linux IP Virtual Server. It uses hash tables for O(1) lookups, supporting massive scale.
3. The Flow
When your Pod sends a packet to 10.96.0.1:
- The packet hits the node’s network interface.
- DNAT (Destination NAT) rules trigger.
- The destination IP is rewritten from
10.96.0.1to10.1.0.5(one of the backing Pods). - The packet is routed to the Pod.
[!TIP] Client-Side Balancing: Kubernetes Services perform client-side load balancing. The translation happens on the source node before the packet even leaves the machine.
5. Service Discovery: DNS
Kubernetes runs a DNS server (CoreDNS) as a Deployment. Every Service gets a DNS record.
| Record Type | Format | Example |
|---|---|---|
| A Record | <svc>.<ns>.svc.cluster.local |
backend.default.svc.cluster.local |
| SRV Record | _<port>._<proto>.<svc>.<ns>... |
_http._tcp.backend.default... |
When you run curl http://backend inside a Pod:
- The Pod’s
/etc/resolv.confpoints to the CoreDNS Service IP. - CoreDNS resolves
backendto the ClusterIP10.96.0.1. - The request is sent to
10.96.0.1. iptablescaptures it and redirects it to a real Pod IP.
6. Code Example: Service Manifest & Access
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: finance
spec:
type: ClusterIP # Default
selector:
app: payment # Matches Pod labels
ports:
- protocol: TCP
port: 80 # Service exposes port 80
targetPort: 8080 # Container listens on 8080
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class PaymentClient {
public static void main(String[] args) {
// Accessing the service via its DNS name
// Format: http://<service-name>.<namespace>
String serviceUrl = "http://payment-service.finance";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(serviceUrl + "/process"))
.GET()
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}
}
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// Accessing the service via its DNS name
// Format: http://<service-name>.<namespace>
serviceUrl := "http://payment-service.finance"
resp, err := http.Get(serviceUrl + "/process")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("Response:", string(body))
}