Certified Kubernetes Administrator (CKA) #18 Networking 1: Service (ClusterIP/NodePort/LoadBalancer/ExternalName)
#17 Storage 2 wrapped up the storage domain. Now we enter the Services and Networking domain, which makes up 20% of the CKA exam. This first post in that domain covers the foundation of networking: the Service.
Pods die and come back at any time, and each time their IP changes. You can’t send traffic directly to a shifting Pod IP. A Service is the abstraction that puts a stable virtual IP and DNS name in front of a shifting set of Pods. If you first met this topic conceptually in K8s Basics #5, this post revisits the same subject from the CKA operator’s point of view. We’ll lay out how a Service picks Pods with a selector, builds Endpoints, and how kube-proxy implements that as node-level rules, plus how to trace where the path broke when a Service stops working.
The problem a Service solves #
Say you have 3 Pods launched by a Deployment. Those 3 Pods die and come back on every rolling update, node failure, or scale change, and each time they get a new IP. A client that hard-codes a specific Pod IP breaks immediately.
A Service solves this in three ways.
- A stable virtual IP (ClusterIP). As long as the Service object lives, the IP doesn’t change.
- A DNS name. CoreDNS resolves
service-name.namespace.svc.cluster.localto the ClusterIP. - Load balancing. It spreads traffic across the multiple Pods caught by the selector.
The key point is that a Service does not point at Pods directly. A Service merely declares a condition called a label selector, and the actual list of Pod IPs matching that condition is filled into a separate object called Endpoints (or EndpointSlice). This separation is the starting point for operations and debugging.
selector → Endpoints → kube-proxy #
The path a Service follows before it can carry traffic has three stages. In the CKA, a “the Service isn’t working” problem is almost always one of these three being broken.
| Stage | Actor | What it does |
|---|---|---|
| 1. Select | the Service’s selector | Declares which labeled Pods to target |
| 2. Fill | endpoints controller | Records the IPs of matching, Ready Pods into Endpoints |
| 3. Implement | kube-proxy | Translates Endpoints into the node’s iptables/IPVS rules |
- The Service declares a condition like
selector: app=web. - The endpoints controller finds the IPs of Pods that match that condition and are Ready, and fills them into the Endpoints object. A Pod that hasn’t passed its readiness probe is dropped here.
- Each node’s kube-proxy watches Endpoints and installs iptables (or IPVS) rules that DNAT a packet arriving at the ClusterIP to one of the real Pod IPs.
So the ClusterIP isn’t a real IP that some process listens on — it’s a virtual IP intercepted by the rules kube-proxy planted in the node kernel. Once you know this, it follows naturally that ping ClusterIP failing is normal.
The four Service types #
| Type | Access scope | Behavior | Main use |
|---|---|---|---|
| ClusterIP | Inside the cluster | Virtual IP + DNS, internal load balancing | Default. Internal communication |
| NodePort | A port on every node | On top of ClusterIP, opens port 30000–32767 on each node | External exposure (simple) |
| LoadBalancer | External LB IP | On top of NodePort, provisions a cloud LB | External exposure in the cloud |
| ExternalName | DNS alias | No selector, returns only a CNAME | Referencing a service outside the cluster |
Higher types include the lower ones. NodePort keeps the ClusterIP as-is and adds a node port on top; LoadBalancer puts a cloud load balancer on top of NodePort. Only ExternalName has a different character. With no selector and no Endpoints, it merely returns a CNAME for an external domain at the DNS stage.
Distinguishing port / targetPort / nodePort #
Mix up the three port fields and traffic goes to the wrong place.
- port: The port the Service itself opens on the ClusterIP. A client connects to
service-name:port. - targetPort: The container port on the Pod where traffic ultimately lands. If omitted, it’s taken to be the same value as
port. - nodePort: The port each node opens to the outside (30000–32767) on NodePort/LoadBalancer. If omitted, one is auto-assigned from the range.
Traced as a flow: nodeIP:nodePort (outside) → ClusterIP:port (inside) → PodIP:targetPort.
ClusterIP example #
Let’s look at the most basic type, ClusterIP, in YAML. It forwards traffic on port 80 to container port 8080 on Pods carrying the app=web label.
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # default even if omitted
selector:
app: web # target Pods carrying this label
ports:
- port: 80 # the port the Service opens (ClusterIP:80)
targetPort: 8080 # the Pod's container portAfter creating it, confirm via Endpoints that the selector actually caught Pods.
k get svc web
k get endpoints web # or k get endpointslices
# connectivity test from inside the cluster (temporary Pod)
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
wget -qO- http://web:80If even one Pod IP shows up in k get endpoints web, the selector is hooked up correctly. If it’s <none>, traffic has nowhere to go.
NodePort example #
Use NodePort when you need external access directly via a node IP.
apiVersion: v1
kind: Service
metadata:
name: web-np
spec:
type: NodePort
selector:
app: web
ports:
- port: 80 # ClusterIP port
targetPort: 8080 # Pod container port
nodePort: 30080 # external port every node opens (30000-32767)Now inside the cluster you reach the same Pods via web-np:80, and outside the cluster via <any node IP>:30080. If you don’t specify nodePort, one is auto-picked from the 30000–32767 range. When the exam asks for a specific port, you must specify it.
k get svc web-np
curl http://<nodeIP>:30080k expose: creating a Service with a single command #
In the exam, k expose or k create service is often faster than hand-writing YAML. It’s the quickest way to put a Service in front of an existing Deployment.
# expose a Deployment as ClusterIP (selector is auto-extracted from the Deployment's labels)
k expose deployment web --port=80 --target-port=8080
# as NodePort
k expose deployment web --port=80 --target-port=8080 --type=NodePort
# when you want to inspect only the YAML before creating
k expose deployment web --port=80 --target-port=8080 $doThe big advantage of k expose is that it fills in the selector automatically. If you hand-write a selector and make a typo, Endpoints comes up empty — but expose carries over the target object’s labels as-is, so that mistake can’t happen. There’s no option to pin nodePort to a specific value, though, so if you need a fixed port, dump the YAML with $do, add the nodePort line, and apply it.
headless Service #
Setting clusterIP: None makes a headless Service. It gets no virtual IP, and on a DNS lookup it returns each Pod’s IP directly rather than one ClusterIP.
apiVersion: v1
kind: Service
metadata:
name: web-hl
spec:
clusterIP: None # headless
selector:
app: web
ports:
- port: 80
targetPort: 8080Since kube-proxy doesn’t get involved, there’s no load balancing and no DNAT. The client picks directly from the list of Pod IPs it got from DNS. Workloads that need to reach individual Pods by a stable, unique name, like a StatefulSet, pair up with a headless Service. StatefulSet was covered in #11.
ExternalName: attaching an alias to a service outside the cluster #
ExternalName has no selector and no Endpoints. CoreDNS simply returns this Service name as a CNAME for an external domain.
apiVersion: v1
kind: Service
metadata:
name: db
spec:
type: ExternalName
externalName: db.prod.example.comAn app inside the cluster looks up db.<namespace>.svc.cluster.local, and CoreDNS directs it to db.prod.example.com. This is useful for presenting an endpoint outside the cluster — such as a managed database — behind an in-cluster Service name.
Operations: the order for tracing when a Service isn’t working #
In CKA troubleshooting, “I can’t reach the app” is mostly solved by finding the broken spot along this path. Let’s check from the top down.
- Does the Service exist with the right type/ports?
k get svc web
k describe svc web # check Selector, Port, TargetPort- Are Pod IPs filled into Endpoints? This is the most common failure point.
k get endpoints webIf ENDPOINTS is <none>, traffic has nowhere to go. The cause is almost always one of two.
- Selector mismatch: The Service’s selector differs from the Pod’s labels. Put
k describe svc’s Selector andk get pods --show-labelsside by side and compare. - Pod is NotReady: A Pod that hasn’t passed its readiness probe is dropped from Endpoints. Check the READY column with
k get pods.
# directly compare the selector against actual labels
k describe svc web | grep -i selector
k get pods --show-labels | grep web- Does targetPort match the port the container listens on?
When Endpoints has IPs but the connection still fails, it’s often because targetPort differs from the container’s actual port. Poke the Pod directly at the IP:port shown in Endpoints.
k get endpoints web -o wide
k run tmp --image=busybox:1.36 --rm -it --restart=Never -- \
wget -qO- http://<PodIP>:<targetPort>- Is kube-proxy running?
If ClusterIP works but only NodePort fails, or the result differs from node to node, suspect that node’s kube-proxy.
k get pods -n kube-system -l k8s-app=kube-proxy -o wide
k logs -n kube-system <kube-proxy-pod>Commit this sequence to memory and “the Service isn’t working” turns from a vague problem into a problem with a checklist.
Creating a selector mismatch and fixing it #
Let’s reproduce the most frequent mistake directly. The Pod label is app=web, but the Service selector was wrongly written as app=webb.
# Endpoints is empty → suspect a selector mismatch
k get endpoints web
# NAME ENDPOINTS AGE
# web <none> 30s
# correct the selector to the right label
k patch svc web -p '{"spec":{"selector":{"app":"web"}}}'
# confirm it got refilled
k get endpoints webThe moment you fix the selector, the endpoints controller refills the Pod IPs and kube-proxy updates the rules. No separate restart is needed.
Exam points #
- Look at Endpoints first. The first command for a Service problem is almost always
k get endpoints. If it’s empty, suspect the selector or readiness; if it’s filled, suspect targetPort or kube-proxy. - Distinguish port / targetPort / nodePort.
portis the Service,targetPortis the Pod,nodePortis the node. Mixing up the three leads straight to a wrong answer. - The NodePort range is 30000–32767. When the exam asks for a specific nodePort, specify it; a value outside the range is rejected.
k exposeprevents selector typos. When exposing an existing workload, auto-extracting withexposeinstead of hand-writing the selector is faster and safer.- Headless is
clusterIP: None. The behavior of returning Pod IPs directly without a virtual IP, and its pairing with StatefulSet, come up. - You can’t ping a ClusterIP. A ClusterIP is a virtual IP, so it doesn’t answer ICMP. Verify with
wget/curl, not ping.
Wrap-up #
What this post locked in:
- A Service is the abstraction of a stable virtual IP, DNS, and load balancing in front of a shifting set of Pods.
- The behavior has three stages: selector (Service) → Endpoints (endpoints controller) → rules (kube-proxy). A Service does not point at Pods directly.
- The types are ClusterIP (default, internal) / NodePort (node port 30000–32767) / LoadBalancer (cloud LB) / ExternalName (CNAME), with each higher one including the lower.
- The ports are distinguished as port (Service) / targetPort (Pod) / nodePort (node).
clusterIP: Noneis headless and returns Pod IPs directly, andk exposematches the selector automatically.- When a Service isn’t working, trace in the order Endpoints → selector/readiness → targetPort → kube-proxy.
Next: Networking 2 #
With a Service, we built a stable entrance to a set of Pods. But NodePort’s port numbers are messy, and LoadBalancer is expensive because it spins up one LB per service. We need a tool that gathers many services behind a single entrance at the HTTP level, based on host and path.
In #19 Networking 2: Ingress, IngressClass, TLS, we’ll lay out — from an operational angle — how Ingress does L7 routing, how IngressClass picks which controller handles it, and how to terminate HTTPS by attaching a TLS certificate as a Secret.