Asterrr's Handbook

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

TypeYou getNotes
ClusterIPA virtual IP from the Service CIDR plus a DNS nameReachable only inside the cluster
NodePortClusterIP plus a port on every node, 30000–32767nodePort is picked for you unless you set it
LoadBalancerNodePort plus an external load balancerEXTERNAL-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 IPsStatefulSets use it to give each Pod a stable name
ExternalNameA DNS CNAME to externalNameNo 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: TCP
  • targetPort defaults to port. Using a named port (containerPort with name: web) lets each Pod version change the number without touching the Service.
  • Clients inside the cluster use port. Clients from outside use nodeIP: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:7000

Exam 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

FieldEffect
externalTrafficPolicy: LocalNodePort/LB traffic goes only to Pods on the receiving node. Preserves the client source IP; nodes without a Pod drop it
internalTrafficPolicy: LocalIn-cluster traffic only to Pods on the same node (node-local agents)
sessionAffinity: ClientIPSame client IP sticks to one Pod (default timeout 3 hours)
trafficDistribution: PreferSameZonePrefer endpoints in the client's zone, fall back cluster-wide. PreferClose is the older alias

Scenarios

Scenario
Deployment relay in namespace edge runs Pods labelled app=relay,ver=2 listening on 8443. A Service relay with selector app=relay,ver=1, port 443 and targetPort 8443 was created last week. Clients now get 'connection refused' immediately at relay.edge:443. What do you check FIRST?
Scenario
A cluster on bare-metal servers has a Service of type LoadBalancer whose EXTERNAL-IP has shown pending for an hour. A partner must reach the app today on a fixed port on the nodes' public addresses. What is the quickest working option?
Scenario
An app behind a NodePort Service must log each client's real public IP, but logs show node IPs. Which change fixes this?

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.

Further reading

On this page