Kubernetes Services: ClusterIP vs NodePort vs LoadBalancer vs ExternalName
Video: Day 9/40 — Kubernetes Services Explained • https://www.youtube.com/watch?v=tHAQWLKMTB0 • Duration: ~46 min
Key terms
| Term | Meaning |
|---|---|
| Service | Stable network endpoint for a set of pods |
| ClusterIP | Internal-only virtual IP (default type) |
| NodePort | Exposes a port on every node |
| LoadBalancer | Provisions an external cloud load balancer |
| ExternalName | Maps a Service to an external DNS CNAME |
| Selector | Labels picking the backend pods |
| Endpoints | The pod IPs currently behind a Service |
| port/targetPort/nodePort | Service / container / node port numbers |
Problem & solution
Pods are ephemeral and get a new IP each time they are recreated, so nothing can reliably address them. We need a stable virtual IP and DNS name that load-balances across a constantly changing set of matching pods.
Solution: Put a Service (ClusterIP/NodePort/LoadBalancer) in front of pods for a stable IP/DNS that load-balances to healthy pods.
The analogy
At our port, the actual ships come and go constantly, different vessels tie up at
different spots every day. If you told a customer "your goods are on the ship currently
at the third buoy," they'd be lost the moment it sailed. So the port gives each route a
permanent pier number: customers always go to "Pier 7," and the port quietly directs
them to whichever ships are berthed there right now. A Kubernetes Service is that
permanent pier number, a stable IP and DNS name, while the pods behind it are the
ever-changing ships. The Service's selector is the rule "all ships flying the
app=web flag dock at this pier."
Where this fits in the cluster
The same cluster entities appear in every day's notes; the diagram below shows where this day's topic fits.
Why Services exist
Pods are ephemeral — they die and get recreated with new IPs. You can't rely on a pod IP. A Service gives a stable virtual IP + DNS name and load-balances across the matching pods.
Pods run on nodes. Each pod gets its own IP; the container listens on a
containerPort (e.g. 8080). Nodes have their own IPs too.
Graph legend — a real nginx Deployment fronted by a Service vs hit directly:
| Graph node | Maps to | What it does |
|---|---|---|
| nginx pod 10.1.1.5 port 80 | a bare nginx pod's podIP | The ephemeral IP that changes on every recreate |
| Service nginx ClusterIP 10.96.0.5 | Service.spec (type ClusterIP) | Stable virtual IP + DNS in front of the nginx pods |
| nginx pod ...port 80 (Node A/B) | the nginx Deployment's pods | Real backends selected by the Service, on any node |
| load-balanced across nodes | kube-proxy / Endpoints | Spreads requests over the healthy nginx pods |
How a Service finds its pods: labels & selectors
A Service has no hard-coded pod list — its selector matches pod labels, and the matching pod IPs become the Service's Endpoints.
Graph legend — how the nginx Service discovers its pods:
| Graph node | Maps to | What it does |
|---|---|---|
| Service nginx spec.selector app=nginx | Service.spec.selector | Label query that picks the backend nginx pods |
| nginx pod app=nginx | the nginx pods' metadata.labels | Pods carrying app=nginx are matched |
| Endpoints = podIP, podIP, podIP | the auto-created Endpoints/EndpointSlice | The live list of matching nginx pod IPs |
kubectl get endpoints nginx # the actual nginx pod IPs behind the service
The 4 Service types
A Service's type field decides who can reach it and how. There are four
types, from most internal to most external: ClusterIP (inside the cluster
only), NodePort (reachable on each node's IP), LoadBalancer (a public
cloud IP), and ExternalName (a DNS alias to something outside the cluster).
The next four subsections take them one at a time.
1) ClusterIP (default) — internal only
Gives a stable virtual IP reachable only inside the cluster.
Use for pod-to-pod / internal microservice traffic.
Graph legend — ClusterIP keeps nginx internal-only:
| Graph node | Maps to | What it does |
|---|---|---|
| Service nginx type=ClusterIP | Service.spec.type: ClusterIP | Internal-only virtual IP, the default Service type |
| nginx pod | the nginx Deployment's pods | In-cluster backends the ClusterIP load-balances to |
| client pod | another in-cluster workload | Reaches nginx by the Service name/ClusterIP, never from outside |
2) NodePort — expose on every node's IP at a high port
Opens the same high port on every node, forwarding outside traffic to the pods.
Graph legend — NodePort exposes nginx on every node:
| Graph node | Maps to | What it does |
|---|---|---|
| Service nginx type=NodePort | Service.spec.type: NodePort | Opens the same high port on every node for nginx |
| Node A/B port 30007 | Service.spec.ports[].nodePort | The 30000-32767 port each node forwards inward |
| nginx pod | the nginx Deployment's pods | Real backends the NodePort routes to via ClusterIP |
3) LoadBalancer — cloud external LB
Asks the cloud provider for a public IP and front-ends the service with a managed load balancer.
Graph legend — LoadBalancer fronts nginx with a cloud LB:
| Graph node | Maps to | What it does |
|---|---|---|
| Cloud LB public IP | Service.spec.type: LoadBalancer | Cloud-provisioned external load balancer with a public IP |
| Service nginx NodePort layer | the auto-allocated nodePort | The LoadBalancer forwards to this node port under the hood |
| Service nginx ClusterIP layer | the Service's clusterIP | The internal VIP the NodePort forwards to |
| nginx pods | the nginx Deployment's pods | Final backends serving the request |
4) ExternalName — DNS alias to an external host
Maps the service name to an external DNS host via a CNAME — no pods, no proxying.
Graph legend — ExternalName aliases an external managed Postgres:
| Graph node | Maps to | What it does |
|---|---|---|
| Service postgres type=ExternalName | Service.spec.type: ExternalName | A Service with no selector/pods, just a DNS CNAME |
| mydb...rds.amazonaws.com | Service.spec.externalName | The real external host (e.g. an AWS RDS Postgres endpoint) |
| app pod | an in-cluster client | Resolves postgres to the external DB via CoreDNS |
Layering (each type builds on the previous)
Kubernetes Services come in three main types, and they are not separate ideas — each one builds on the type beneath it. ClusterIP gives a Service a stable internal-only address; NodePort adds a fixed port on every node on top of that ClusterIP; LoadBalancer adds a cloud load balancer on top of the NodePort. So choosing LoadBalancer automatically gives you the NodePort and ClusterIP underneath — that stacking is the "layering" this section shows.
Graph legend — each Service type wraps the one below:
| Graph node | Maps to | What it does |
|---|---|---|
| LoadBalancer cloud public IP | type: LoadBalancer | Outermost layer: cloud LB with a public IP |
| NodePort NodeIP 30007 | type: NodePort | Middle layer: a port on every node |
| ClusterIP internal VIP | type: ClusterIP | Base layer: the internal virtual IP |
| nginx Pods | the nginx Deployment's pods | The real backends at the bottom of the stack |
Service YAML examples
Minimal manifests for the two types you create most often.
ClusterIP (in front of a real nginx Deployment, image nginx:1.27, container port 80):
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80 # service port
targetPort: 80 # nginx container port
NodePort:
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30007 # optional; auto-assigned if omitted
port vs targetPort vs nodePort
These three ports map the hop from node, to service, to container — keep them straight.
Example mapping: 192.168.0.11:30007 (NodeIP:nodePort) to 10.96.0.5:80 (svc:port) to 10.1.1.8:80 (podIP:targetPort).
Graph legend — the three ports for the nginx Service:
| Graph node | Maps to | What it does |
|---|---|---|
| nodePort 30007 | Service.spec.ports[].nodePort | External port on each Node IP (NodePort/LoadBalancer only) |
| port 80 on the Service ClusterIP | Service.spec.ports[].port | The port the Service VIP listens on in-cluster |
| targetPort 80 on the nginx Pod | Service.spec.ports[].targetPort | nginx's containerPort that actually serves traffic |
Commands
Everyday commands to expose a Deployment, inspect the Service, and test it from inside the cluster.
kubectl create deployment nginx --image=nginx:1.27 --replicas=3
kubectl expose deployment nginx --port=80 --target-port=80 --type=NodePort
kubectl get svc
kubectl get svc nginx -o wide
kubectl describe svc nginx
# test internal DNS from another pod
kubectl run tmp --image=busybox -it --rm -- wget -qO- nginx:80
Service DNS name
Every Service gets an in-cluster DNS name built from its name, namespace, and the cluster domain.
nginx . default . svc . cluster.local ─── ─────── ─── ───────────── service namespace type cluster name marker domain e.g. nginx.default.svc.cluster.local
End-to-end flow
A request reaches a stable Service and is load-balanced across the healthy pods picked by its selector.
Graph legend — a request reaching the real nginx Service end to end:
| Graph node | Maps to | What it does |
|---|---|---|
| Client pod: curl nginx | an in-cluster caller | Resolves the Service DNS name and connects |
| Service nginx ClusterIP + DNS | Service.spec (ClusterIP) | Stable VIP/name for the nginx Deployment |
| kube-proxy rules | kube-proxy iptables/IPVS | Programs the load-balancing rules on the node |
| Endpoints from selector app=nginx | the Service's Endpoints | Live nginx pod IPs matched by the selector |
| nginx Pod 1/2/3 | the nginx Deployment's pods | The healthy backends that serve the response |
Key takeaways
- Services give a stable IP/DNS + load balancing over ephemeral pods.
- ClusterIP internal, NodePort node IPs, LoadBalancer cloud public, ExternalName DNS alias.
- Remember the port / targetPort / nodePort trio.
Checklist
- [ ] Exposed a Deployment as ClusterIP and reached it from another pod
- [ ] Created a NodePort and hit it on a node IP
- [ ] Inspected
kubectl get endpoints - [ ] Can explain port vs targetPort vs nodePort