Certified Kubernetes Application Developer (CKAD) #18 Services: ClusterIP, NodePort, LoadBalancer, ExternalName
In #17 Volumes you attached data to a Pod; this time we cover how to connect to that Pod reliably. While a Deployment keeps Pods running, those Pods are constantly created and destroyed. A rolling update swaps out IPs wholesale, and when a node dies a fresh Pod comes up on another node. The object that plants an unchanging entry point in front of this shifting set of Pods is the Service.
A Service picks Pods with a selector, automatically tracks the IP list of the chosen Pods, and exposes to clients only a fixed name and a single virtual IP. In this post we’ll organize how a Service works, its four types, the distinction between the three kinds of ports, the DNS rules, and the endpoint debugging that shows up often on the exam.
Why you need a Service #
A Pod’s IP is disposable. When a Pod restarts or moves to another node, its IP changes, and when a Deployment has several replicas the client can’t even know which IP to send to. So code that calls a Pod IP directly breaks immediately.
A Service solves this in two ways.
- A fixed entry point. A Service is given an unchanging name (DNS) and a ClusterIP. No matter how much the Pods behind it change, the client sends to the same address.
- Load balancing. It automatically distributes requests across the several Pods that match the selector.
If you want a broader grounding in Kubernetes fundamentals, K8s Practical Track #5 Services and Networking pairs well with this. This post focuses on commands and manifests from the CKAD hands-on perspective.
Selectors, labels, and Endpoints #
A Service does not target Pods directly. Instead, you write a label condition in spec.selector, and the cluster finds the Pods that match that condition for you. The object that actually holds this connection is Endpoints (or EndpointSlice).
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web # Select all Pods carrying this label
ports:
- port: 80
targetPort: 8080The Service above finds Pods carrying the app=web label and automatically fills the IPs and ports of those Pods into Endpoints. When a new Pod comes up it’s added to Endpoints, and when one disappears it’s dropped. This automatic management is the core of a Service.
# Check the actual list of Pods the Service points to
k get endpoints web
k get endpointslices -l kubernetes.io/service-name=webIf the selector is off from the Pod’s label by even one character, Endpoints comes up empty, and then the Service receives the connection but has nowhere to send it. We’ll revisit this case in the debugging section below.
The four Service types #
Services split into four types according to how far they expose to the outside. If you don’t specify a type, the default is ClusterIP.
| Type | Access scope | Behavior | Main use |
|---|---|---|---|
| ClusterIP | Inside the cluster | Assigns a single virtual IP, reachable only internally | Communication between internal microservices |
| NodePort | Node IP:port | Opens the same port (30000–32767) on every node | External exposure, testing in environments without an LB |
| LoadBalancer | External LB IP | Provisions a cloud LB, wrapping a NodePort | Exposing an external service on the cloud |
| ExternalName | DNS CNAME | No selector; returns only a CNAME to an external domain | Giving an alias to a service outside the cluster |
These four types are easiest to understand as a containment relationship. LoadBalancer contains NodePort, and NodePort contains ClusterIP. That is, creating a NodePort also creates a ClusterIP, and creating a LoadBalancer creates both a NodePort and a ClusterIP. Only ExternalName is the odd one out, with neither a selector nor a ClusterIP — it’s simply an object that returns a DNS response as a CNAME.
ClusterIP #
The most basic type. It gets a cluster-internal-only virtual IP and can’t be reached from outside. Microservices almost always use this type when calling one another.
NodePort #
It opens the same port on every node, handing traffic that arrives on a node’s IP and that port over to the Service. The port range is 30000–32767, and if you don’t specify one, it’s automatically allocated from this range. It’s useful for quick external exposure or testing in an environment without an LB.
LoadBalancer #
It provisions an external load balancer from the cloud provider (AWS, GCP, Azure, and so on). An external IP is assigned, and traffic arriving on that IP is forwarded through a NodePort to the Pods. In a non-cloud environment, the external IP may stay at <pending>.
ExternalName #
It has neither a selector nor a ClusterIP. Instead, it returns a CNAME record pointing to the external domain you write in spec.externalName. You use it to let code inside the cluster call a resource such as an external database by an internal name.
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ExternalName
externalName: db.example.com # Return only a CNAME to this nameport vs targetPort vs nodePort #
The most confusing spot in CKAD is the three kinds of ports. Each one points to a different target.
| Field | Whose port is it | Description |
|---|---|---|
| port | The Service itself | The port the client uses to reach the Service |
| targetPort | The Pod container | The container port the Service hands traffic to |
| nodePort | The node | The port the node opens externally on NodePort/LoadBalancer (30000–32767) |
The flow goes like this. Traffic arriving on the nodePort of the outside world or a node passes through the Service’s port and is finally forwarded to the Pod’s targetPort. If you omit targetPort, it’s treated as the same value as port. If the container listens on 8080 but you want to open the Service’s port on 80, you must explicitly specify targetPort: 8080.
Creating a Service imperatively #
On the exam, pulling a skeleton with a generator is faster than writing the manifest by hand. Memorize these two commands and you can handle most Service tasks.
# 1) Expose an existing Deployment (the selector is auto-extracted from the Deployment labels)
k expose deploy web --port=80 --target-port=8080
# 2) Create a Service directly (the selector must be specified manually)
k create svc clusterip web --tcp=80:8080
k create svc nodeport web --tcp=80:8080 --node-port=30080
# Pull only the manifest skeleton with dry-run
k expose deploy web --port=80 --target-port=8080 $do > svc.yamlk expose brings the target workload’s labels over as the selector directly, so it takes the least effort. To change the type, though, add an option like --type=NodePort, or fix it with k edit after creating it.
# Expose and specify the type at the same time
k expose deploy web --port=80 --target-port=8080 --type=NodePortYAML examples #
ClusterIP #
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP # Default if omitted
selector:
app: web
ports:
- protocol: TCP
port: 80 # Service port
targetPort: 8080 # Container portOther Pods inside the cluster reach this Service by the name web or web.<namespace>.
NodePort #
apiVersion: v1
kind: Service
metadata:
name: web-np
spec:
type: NodePort
selector:
app: web
ports:
- protocol: TCP
port: 80 # Service port used inside the cluster
targetPort: 8080 # Container port
nodePort: 30080 # Port the node opens externally (30000-32767, auto if omitted)This Service hands traffic arriving on port 30080 of every node over to the 8080 port of app=web Pods. From outside, you reach it at <node IP>:30080.
headless Service #
Specifying clusterIP: None makes a headless Service that gets no virtual IP. In this case, resolving the Service name in DNS returns not a single ClusterIP but the list of each Pod’s IP as-is. Used together with a StatefulSet, it’s often used to give each Pod a fixed DNS name in the form <pod>.<service>.<namespace>.svc.cluster.local.
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # headless
selector:
app: db
ports:
- port: 5432Cluster DNS #
When you create a Service, CoreDNS automatically registers a DNS record. The canonical name (FQDN) follows this format.
<service>.<namespace>.svc.cluster.localFor example, the web Service in the prod namespace resolves as web.prod.svc.cluster.local. A Pod within the same namespace can reach it with the short name web alone, and when calling a Service in another namespace you attach at least the namespace, as in web.prod.
# Check DNS from a temporary Pod
k run tmp --image=busybox --rm -it --restart=Never -- nslookup web.prod.svc.cluster.local
# In the same namespace, use the short name
k run tmp --image=busybox --rm -it --restart=Never -- wget -qO- webDebugging: when endpoints come up empty #
When a “can’t connect to the Service” question comes up on the exam, the first thing you check is Endpoints. If Endpoints is empty, it means the Service couldn’t find a Pod to point to, and it’s almost always due to a mismatch between the selector and the Pod label.
# 1) Are Pod IPs filled into Endpoints?
k get endpoints web
# 2) Check the Service's selector
k describe svc web | grep -i selector
# 3) Check the actual Pod's labels
k get pods --show-labelsFor example, if the Service’s selector is app: web but the Pod’s label is app: webapp, the two values differ, so Endpoints comes up empty and the connection fails too. In that case, fix the Service’s selector or align the Pod’s label.
# Align the Pod label with the Service selector
k label pod web-xxxxx app=web --overwriteAnother cause of empty endpoints is when targetPort differs from the container’s actual listen port, or when a readiness probe fails so the Pod is NotReady and is excluded from Endpoints. Splitting the diagnosis — is Endpoints empty, or is it filled but the port is wrong — lets you narrow the cause quickly.
# Check whether you reach the Service with a temporary Pod
k run probe --image=busybox --rm -it --restart=Never -- wget -qO- web:80Exam points #
- The default type is ClusterIP. NodePort and LoadBalancer contain ClusterIP, and only ExternalName is CNAME-only without a selector and ClusterIP.
- Distinguish the three kinds of ports precisely.
portis the Service,targetPortis the container,nodePortis the node. The nodePort range is 30000–32767. k expose deploy ... --port= --target-port=is the fastest exposure command. It brings the selector over automatically.- headless is
clusterIP: None. Together with a StatefulSet it makes a per-Pod DNS name. - DNS is
<service>.<namespace>.svc.cluster.local. Within the same namespace the short name is enough. - A failed connection starts with
k get endpoints. If it’s empty, suspect a mismatch between the selector and the label.
Wrap-up #
What this post locked in:
- A Service is a fixed entry point in front of a shifting set of Pods; it picks Pods with a selector and manages Endpoints automatically.
- The four types (ClusterIP, NodePort, LoadBalancer, ExternalName) differ in exposure scope, and the first three form a containment relationship.
- Ports split into three kinds:
port(Service),targetPort(container), andnodePort(node). - We learned headless Services and the cluster DNS rules, plus debugging empty endpoints, all through commands.
Next — Ingress and NetworkPolicy #
You’ve put the entry point in place with a Service, but routing external HTTP traffic per path or blocking and opening communication between Pods takes more than a Service alone.
In #19 Ingress and NetworkPolicy we’ll cover Ingress, which bundles several Services with host and path rules, TLS termination, and NetworkPolicy, which starts from default-deny and controls communication with ingress and egress rules — all through YAML and kubectl.