Services
ClusterIP, NodePort, LoadBalancer, headless and ExternalName Services, the port, targetPort and nodePort mapping, EndpointSlices, traffic policies, and fixing a Service that has no endpoints.
Exam tasks: 3.3 (ClusterIP, NodePort, LoadBalancer service types and endpoints), with 5.5 for Services that don't answer
The decision: who needs to reach this app (other Pods, anything that can reach a node, the internet, or a StatefulSet peer by name), and which Service type gives exactly that?
The types
| Type | You get | Notes |
|---|---|---|
ClusterIP | A virtual IP from the Service CIDR plus a DNS name | Reachable only inside the cluster |
NodePort | ClusterIP plus a port on every node, 30000–32767 | nodePort is picked for you unless you set it |
LoadBalancer | NodePort plus an external load balancer | EXTERNAL-IP stays <pending> without a cloud controller or MetalLB-style provider |
Headless (clusterIP: None) | No virtual IP, no kube-proxy rules. DNS returns the ready Pod IPs | StatefulSets use it to give each Pod a stable name |
ExternalName | A DNS CNAME to externalName | No proxying, no ports, no selector |
Ports: three numbers, three meanings
apiVersion: v1
kind: Service
metadata:
name: tracker
namespace: fleet
spec:
type: NodePort
selector:
app: tracker # must match the Pod labels, not the Deployment's
ports:
- name: http # required once there is more than one port
port: 80 # the Service's own port: tracker.fleet:80
targetPort: web # the container port, by number or by name
nodePort: 31080 # optional, NodePort and LoadBalancer only
protocol: TCPtargetPortdefaults toport. Using a named port (containerPortwithname: web) lets each Pod version change the number without touching the Service.- Clients inside the cluster use
port. Clients from outside usenodeIP:nodePort.
Wrong number in the wrong field
The most common Service bug is targetPort set to the Service port instead of the port the container listens on.
The Service has endpoints and looks healthy, but connections are refused. Compare targetPort with the
container's real listening port (k exec ... -- netstat -tln or the image's docs).
Creating them fast
# from a Deployment: copies its selector
k -n fleet expose deploy tracker --name tracker --port 80 --target-port 8080 --type NodePort
# from scratch: selector becomes app=<name>, so the name must match the Pod label
k -n fleet create service clusterip tracker-int --tcp=80:8080
k -n fleet create service nodeport tracker-ext --tcp=80:8080 --node-port=31080
# headless
k -n fleet create service clusterip tracker-peers --clusterip=None --tcp=7000:7000Exam signal
kubectl expose can't set a specific nodePort. Generate the YAML with --dry-run=client -o yaml, add
nodePort: and apply, or k edit svc afterwards. kubectl create service nodeport --node-port can set it, but
its selector is app=<service-name>, so check that it matches.
Endpoints and EndpointSlices
The control plane keeps an EndpointSlice list of ready Pod IPs and ports for every Service with a selector. kube-proxy and DNS read these.
k -n fleet get endpointslices -l kubernetes.io/service-name=tracker
k -n fleet describe svc tracker # Endpoints: line shows the IPs
k -n fleet get pods -l app=tracker -o wide # do these IPs match?- Pods appear only when they're Ready. A failing readiness probe removes a Pod from the Service without restarting it.
- No endpoints almost always means the selector doesn't match Pod labels, the Pods aren't Ready, or they're in another namespace.
- A Service without a selector gets no automatic endpoints. You create an EndpointSlice yourself, labelled
kubernetes.io/service-name: <svc>, to point at something outside the cluster by IP.
Legacy: use EndpointSlice (discovery.k8s.io/v1) instead
The v1 Endpoints API is deprecated since Kubernetes v1.33 and kubectl get endpoints prints a warning. It still
works, and describe svc still shows an Endpoints line, but write new tooling and selector-less Services against
EndpointSlices.
Traffic knobs worth knowing
| Field | Effect |
|---|---|
externalTrafficPolicy: Local | NodePort/LB traffic goes only to Pods on the receiving node. Preserves the client source IP; nodes without a Pod drop it |
internalTrafficPolicy: Local | In-cluster traffic only to Pods on the same node (node-local agents) |
sessionAffinity: ClientIP | Same client IP sticks to one Pod (default timeout 3 hours) |
trafficDistribution: PreferSameZone | Prefer endpoints in the client's zone, fall back cluster-wide. PreferClose is the older alias |
Scenarios
The selector no longer matches any Pod, so the EndpointSlice is empty and kube-proxy rejects connections to the
ClusterIP. Fix the selector (dropping ver is usually best). DNS clearly resolves because the error is a refusal,
not a lookup failure. kube-proxy and node firewalls aren't involved in in-cluster Service traffic failing this way.
Without a cloud controller or a bare-metal LB provider, nothing fills in the external IP, but a LoadBalancer Service already has a NodePort reachable on every node. ExternalName only creates a CNAME. Headless removes the virtual IP. An Ingress still needs a controller exposed somehow, which is the same problem.
With the default Cluster policy, a node may forward to a Pod on another node and SNATs the packet, hiding the
client. Local keeps traffic on the receiving node and preserves the source IP. Session affinity changes stickiness,
not addressing. internalTrafficPolicy applies to in-cluster clients. Headless Services have no NodePort.
Drill
kubectl config use-context lab-net
In namespace fleet, Deployment tracker runs Pods whose container listens on port 8080 (named web). Create a
Service tracker-np of type NodePort that exposes it on Service port 80 and node port 31080. Confirm it answers
from inside the cluster and on a node.
k -n fleet expose deploy tracker --name tracker-np --port 80 --target-port web --type NodePort \
--dry-run=client -o yaml > tracker-np.yamlEdit the port entry to add the node port:
ports:
- port: 80
protocol: TCP
targetPort: web
nodePort: 31080k apply -f tracker-np.yaml
k -n fleet get svc tracker-np # 80:31080/TCP
k -n fleet get endpointslices -l kubernetes.io/service-name=tracker-np # Pod IPs on 8080
k run t --rm -it --restart=Never --image=busybox:1.36 -- wget -qO- -T 2 http://tracker-np.fleet
NODE_IP=$(k get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
curl -s -m 2 http://$NODE_IP:31080Further reading
Network policies
Default-deny NetworkPolicies, selecting peers by Pod labels, namespace labels and CIDRs, the AND vs OR trap in from and to, and keeping DNS working when you lock down egress.
Gateway API
GatewayClass, Gateway and HTTPRoute (gateway.networking.k8s.io/v1), listeners and allowedRoutes, path, header and method matching, filters, weighted traffic splitting, ReferenceGrant and migrating from Ingress.