Pods are ephemeral — their IPs change on every restart. A Service provides a stable endpoint (a fixed IP and DNS name) that load-balances across a set of Pods. It's the primary mechanism for service discovery and internal/external traffic routing in Kubernetes.
1. ClusterIP — Internal Service Discovery
The default Service type. Creates a virtual IP (ClusterIP) accessible only within the cluster.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # default — can omit
selector:
app: web # finds Pods with this label
ports:
- name: http
port: 80 # Service port (what clients connect to)
targetPort: 8080 # Pod port (where traffic is forwarded)
protocol: TCP
How It Works
port vs targetPort vs containerPort
| Field | Defined On | Meaning |
|---|---|---|
port | Service | The port clients use to reach the Service |
targetPort | Service | The port on the Pod to forward to (can be a name) |
containerPort | Pod spec | Informational only — doesn't affect routing |
# targetPort can reference a named port:
spec:
ports:
- port: 80
targetPort: http-server # ← references the Pod's named port
---
# In the Pod:
ports:
- name: http-server
containerPort: 8080
# Useful when different Pod versions use different port numbers
2. NodePort — Expose on Every Node
Opens a static port (30000-32767) on every node's IP. External clients connect via <NodeIP>:<NodePort>.
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80 # ClusterIP port (still created)
targetPort: 8080 # Pod port
nodePort: 30080 # Static port on all nodes (optional — auto-assigned if omitted)
Traffic Flow
Client → 192.168.1.10:30080 (any node IP)
→ DNAT to ClusterIP 10.96.0.50:80
→ DNAT to Pod 10.244.2.8:8080
Note: NodePort also creates a ClusterIP — it's a superset.
3. LoadBalancer — Cloud Integration
Provisions an external load balancer (cloud-provider-specific) that routes traffic to the Service's NodePorts.
apiVersion: v1
kind: Service
metadata:
name: web-lb
annotations:
# Cloud-specific annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb # AWS NLB
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 443
targetPort: 8443
What Gets Created
LoadBalancer (external IP: 52.1.2.3)
└─ NodePort (30080 on all nodes) ← LB sends traffic here
└─ ClusterIP (10.96.0.50:443)
└─ Pods (10.244.x.x:8443)
# Client → 52.1.2.3:443 → Node:30080 → Pod:8443
| Cloud | Default LB Type | Annotation to Control |
|---|---|---|
| AWS | Classic LB (L4) | aws-load-balancer-type: nlb for NLB |
| GCP | Network LB (L4) | Use Ingress for HTTP LB (L7) |
| Azure | Azure LB (L4) | azure-load-balancer-internal: true for internal |
| Bare metal | ❌ Stays Pending | Use MetalLB to get LoadBalancer on bare metal |
Pending forever on bare-metal clusters — there's no cloud API to provision an LB. MetalLB fills this gap for on-prem.
4. Headless Services (clusterIP: None)
No virtual IP. DNS returns the individual Pod IPs directly. Used for StatefulSets and client-side load balancing.
apiVersion: v1
kind: Service
metadata:
name: db-headless
spec:
clusterIP: None # ← Headless
selector:
app: postgres
ports:
- port: 5432
# DNS query returns ALL Pod IPs: nslookup db-headless.default.svc.cluster.local # 10.244.1.5 # 10.244.2.8 # 10.244.3.2 # With StatefulSet: individual Pod DNS records too: # postgres-0.db-headless.default.svc.cluster.local → 10.244.1.5
| Normal Service | Headless Service |
|---|---|
| DNS returns 1 IP (ClusterIP) | DNS returns all Pod IPs |
| Load balancing by kube-proxy | Load balancing by client (or none) |
| Single stable VIP | No VIP — direct Pod access |
| Use: generic services | Use: StatefulSets, peer discovery, gRPC |
5. ExternalName — DNS Alias
Maps a Service name to an external DNS name. No proxying, no ClusterIP — just a CNAME record.
apiVersion: v1 kind: Service metadata: name: external-db spec: type: ExternalName externalName: prod-db.abc123.us-east-1.rds.amazonaws.com
# Inside the cluster: nslookup external-db.default.svc.cluster.local # → CNAME: prod-db.abc123.us-east-1.rds.amazonaws.com # Code just connects to "external-db:5432" — unaware it's external
Use case: Abstract external dependencies behind a K8s Service name. If you later migrate the database into the cluster, just change the Service from ExternalName to ClusterIP — no code changes needed.
6. Advanced Service Features
Session Affinity
spec:
sessionAffinity: ClientIP # Same client → same Pod
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hours (default)
# Uses source IP to pin connections. Useful for stateful apps
# that store session in memory (not recommended — use external session store)
externalTrafficPolicy
| Policy | Behavior | Trade-off |
|---|---|---|
Cluster (default) | Traffic can hop to any node to reach a Pod | Even distribution, but source IP is lost (SNAT) |
Local | Traffic only goes to Pods on the receiving node | Preserves source IP, but uneven distribution (nodes without Pods get no traffic) |
spec: type: LoadBalancer externalTrafficPolicy: Local # Preserve client source IP # The LB health-checks which nodes have Pods — only sends traffic there # Pods on other nodes are unreachable via this path
Local when you need the client's real IP (for rate limiting, geo-routing, access logs). Use Cluster when even distribution matters more than source IP preservation. With Local, ensure enough Pods are spread across nodes to avoid overloading specific nodes.
Topology Aware Hints (K8s 1.23+)
metadata:
annotations:
service.kubernetes.io/topology-mode: Auto
# kube-proxy routes traffic to Pods in the SAME zone preferentially
# Reduces cross-zone data transfer costs (significant on cloud)
# Falls back to other zones if local zone has insufficient capacity
Multi-Port Services
spec:
ports:
- name: http # name required when multiple ports
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
- name: metrics
port: 9090
targetPort: 9090
7. EndpointSlices — How Services Find Pods
When a Service selector matches Pods, Kubernetes creates EndpointSlice objects listing the backing Pod IPs. kube-proxy watches these to build its routing rules.
# Inspect endpoints: kubectl get endpointslices -l kubernetes.io/service-name=web # NAME ADDRESSTYPE PORTS ENDPOINTS AGE # web-abc IPv4 8080 10.244.1.5,10.244.2.8,... 5d kubectl describe endpointslice web-abc # Endpoints: # - Addresses: 10.244.1.5 # Conditions: Ready=true # TargetRef: Pod/web-abc123 # NodeName: worker-1 # Zone: us-east-1a
Services Without Selectors
You can create a Service that points to external IPs by manually creating Endpoints:
# Service without selector:
apiVersion: v1
kind: Service
metadata:
name: external-api
spec:
ports:
- port: 443
---
# Manual EndpointSlice (or Endpoints):
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: external-api-1
labels:
kubernetes.io/service-name: external-api
addressType: IPv4
ports:
- port: 443
endpoints:
- addresses: ["203.0.113.50", "203.0.113.51"]
Use case: Route to external services (databases, APIs) through a K8s Service name while controlling the endpoint IPs manually.
Summary: Service Types at a Glance
| Type | Accessible From | Gets | Use Case |
|---|---|---|---|
ClusterIP | Inside cluster only | Virtual IP + DNS | Internal microservice communication |
NodePort | Outside via node IP:port | ClusterIP + static port on all nodes | Dev/test, quick external access |
LoadBalancer | Internet/VPC via external IP | NodePort + cloud LB | Production external services |
ExternalName | Inside cluster (DNS only) | CNAME record | Alias for external DNS names |
Headless (clusterIP: None) | Inside cluster (direct Pod IPs) | DNS A records per Pod | StatefulSets, gRPC, peer discovery |
📝 Quiz: Services in Depth
Q1: A Service has port: 80 and targetPort: 3000. A client inside the cluster connects to the Service on port 80. What port does the Pod receive traffic on?
Q2: You create a type: LoadBalancer Service on a bare-metal cluster. What happens?
EXTERNAL-IP stays <pending> forever. There's no cloud controller to provision an external LB. Solution: install MetalLB which provides a LoadBalancer implementation for bare-metal clusters.Q3: Your app needs the real client IP for rate limiting. The Service is type: LoadBalancer. What setting do you need?
externalTrafficPolicy: Local. With the default Cluster policy, the node SNATs the traffic when forwarding to another node, losing the original source IP. With Local, traffic only goes to Pods on the receiving node — no SNAT, original IP preserved.Q4: A gRPC service with 5 replicas uses a normal ClusterIP Service. Clients report all requests go to the same Pod. Why?
Q5: What's the difference between a headless Service and an ExternalName Service?
ExternalName: No selector, no ClusterIP. DNS returns a CNAME pointing to an external hostname. Used to alias external services as cluster-internal names.
Q6: Why does Kubernetes use EndpointSlices instead of the older Endpoints resource?