Pods are temporary: recreate one and its IP changes. A Service gives a group of pods (picked by a label selector) a stable address: a virtual ClusterIP and a DNS name <service>.<namespace>.svc.cluster.local.
apiVersion: v1
kind: Service
metadata: { name: web }
spec:
type: ClusterIP # default
selector: { app: web }
ports:
- port: 80 # Service port
targetPort: 8080 # container port
flowchart LR C["client pod"] -- "web:80 (10.96.28.176)" --> KP["kube-proxy rules<br/>on the node"] KP --> P1["pod 192.168.1.5:8080"] KP --> P2["pod 192.168.2.7:8080"]
- A ClusterIP is only reachable inside the cluster. It is a virtual IP: kube-proxy programs rules (iptables/IPVS) that forward traffic to endpoints (IP:port of Ready pods matching the selector). Since no machine owns that IP,
pingto a ClusterIP usually gets no answer even thoughcurlon the declared port works. portis the Service port;targetPortis the port the container listens on. Mixing them up is very common.- A wrong selector label → no endpoints → the Service does not answer.
- In the same namespace, use the short name
web; across namespaces useweb.prodor the full name. - Since Kubernetes 1.33 the
v1 EndpointsAPI is deprecated; use EndpointSlices (kubectl get endpointslices -l kubernetes.io/service-name=web).
| Command | Meaning |
|---|---|
kubectl expose deployment web --port=80 --target-port=8080 |
Create a ClusterIP Service for a Deployment |
kubectl get svc,endpoints |
ClusterIP and endpoints |
kubectl port-forward svc/web 8080:80 |
Reach the Service from your machine on localhost:8080 |
Named ports and multiple ports
A Service can expose several ports, each with a name, and targetPort may refer to a named container port instead of a number:
ports:
- { name: http, port: 80, targetPort: web }
- { name: metrics, port: 9090, targetPort: 9090 }
Named target ports let you change the container's port number in the pod template without touching the Service. Names are required when there is more than one port.
Session affinity and traffic policy
sessionAffinity: ClientIP sends requests from one client IP to the same pod for a configurable time, a simple form of sticky sessions. internalTrafficPolicy: Local keeps traffic on the same node when a local pod exists, useful for node agents. For HTTP-level stickiness, an ingress controller or service mesh is a better tool.
Headless Services
clusterIP: None creates a headless Service: DNS returns the individual pod IPs instead of one virtual IP, and kube-proxy does no balancing. StatefulSets and client-side load balancers such as gRPC clients use this to talk to specific pods.
Services without selectors
A Service with no selector gets no automatic endpoints. You create an EndpointSlice yourself that points to IP addresses outside the cluster, for example a database on a VM. Pods then use a normal in-cluster name, and you can later move the database into the cluster without changing clients.
Troubleshooting checklist
| Check | Command |
|---|---|
| Does the Service exist with the right ports? | kubectl get svc web -o wide |
| Are there endpoints? | kubectl get endpointslices -l kubernetes.io/service-name=web |
| Do pod labels match the selector? | kubectl get pods -l app=web --show-labels |
| Does DNS resolve? | kubectl run tmp --rm -it --image=busybox:1.36 -- nslookup web |
| Does the pod answer on targetPort? | kubectl port-forward pod/<pod> 8080:8080 then curl localhost:8080 |
Most "Service does not work" problems are an empty endpoint list caused by a label mismatch or failing readiness probes.