CoursesKubernetes fundamentalsThe kinds of Service

The kinds of Service

ClusterIP, NodePort, LoadBalancer.

Beginner10 min · lesson 17 of 24
In plain terms
Service types are how far the phone number reaches: an internal extension (ClusterIP), a line on every building door (NodePort), or a real published public number (LoadBalancer).

Every app you run in Kubernetes eventually faces one plain question. Who is allowed to reach you? A Service answers it. Think of a Service as the phone number for your app: a stable address that always finds the right Pods behind it, even as those Pods come and go. (A Pod is the small wrapper Kubernetes runs your container in. A container is your app bundled up with everything it needs to run. A cluster is the group of machines Kubernetes manages.) A phone number carries something beginners tend to overlook, though. Reach. An office extension only rings inside the building. A published number rings from anywhere. Services behave the same way, and one field decides how far yours reaches. It goes by the name type.

ClusterIP: the internal extension

ClusterIP is the default. Make a Service, say nothing about its type, and this is what you get. It gives your app a stable internal address that only works inside the cluster. Nothing out on the public internet can dial it. That sounds limiting until you notice how little traffic in a real system ever leaves the cluster. Your web app talks to a backend service. That service talks to a database (where the app's data lives). That's all Pod-to-Pod, all internal, and ClusterIP fits it perfectly. You reach for anything bigger only when something outside genuinely needs to get in.

svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 8080

Two lines do the real work here. The selector says which Pods get the traffic: any Pod wearing the label app: web. (A label is a small name tag you stick on Kubernetes objects.) The ports line says callers connect on port 80. (A port is just a numbered door on a machine; one machine has thousands, and each program listens at its own number.) The Service quietly forwards that traffic to port 8080 inside the Pod, where your app actually listens. Apply the file, then ask Kubernetes what it made.

terminal
$ kubectl apply -f svc-clusterip.yaml
service/web created
$ kubectl get svc web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.96.14.201 <none> 80/TCP 5s

Look at the EXTERNAL-IP column. It reads <none>, and that isn't a fault. A ClusterIP Service has no outside address by design. Other Pods reach it at the CLUSTER-IP shown, 10.96.14.201, or simply by its name, web. The outside world sees nothing at all.

A Service is only worth anything if it actually points at running Pods, and there's a one-line way to check. Ask for its endpoints, the list of Pod addresses sitting behind the name.

terminal
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.1.7:8080,10.244.2.4:8080 2m

Two Pod addresses, each on port 8080: the Service is wired up and traffic has somewhere to land. If that column ever reads <none>, the Service is a phone number ringing an empty room, and the cause is almost always a selector whose label matches no Pod. An empty endpoints list is the most common reason a Service 'doesn't work,' and kubectl get endpoints is how you catch it before you waste an afternoon on it.

NodePort and LoadBalancer: opening a door outside

Sometimes traffic really does arrive from outside. Two types open that door, and both build on ClusterIP rather than replacing it. NodePort opens the same port on every node. (A node is one machine in the cluster.) Say a shop has several street entrances. Paint the same door number on all of them and a customer can walk in through any one. You reach the app at any node's IP address plus that shared port. The ports sit in a high, awkward range, 30000 to 32767, and you still need to know a node's address. That makes NodePort fine for a quick test or a bare-metal server (a physical machine you run yourself, with no cloud underneath), but rough for real users.

You can let Kubernetes hand you a NodePort automatically, or pin a specific number yourself. Pin one and that awkward range bites the first time you forget it.

svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: web-node
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 8080
terminal
$ kubectl apply -f svc-nodeport.yaml
The Service "web-node" is invalid: spec.ports[0].nodePort: Invalid value: 8080: provided port is not in the valid range. The range of valid ports is 30000-32767

8080 looks like the obvious choice, but NodePort only accepts 30000 to 32767. Set nodePort to 30080 instead, or drop the line entirely and let Kubernetes pick a free one for you. Dropping it is the easier path unless you have a reason to fix the number. Remove that line and re-apply.

terminal
$ kubectl apply -f svc-nodeport.yaml
service/web-node created

LoadBalancer goes all the way. It asks your cloud provider to build a real load balancer out front, with its own public address, the sort you'd hand to actual customers. (A load balancer is a kind of traffic cop: requests arrive at one address, and it spreads them across your Pods.) On a cloud like AWS, Google Cloud, or Azure, this is the standard way to put an app on the internet. One catch is worth learning early. Each LoadBalancer Service is a separate cloud resource, and the cloud charges you for every single one.

svc-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
name: web-public
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080

Notice how little changed from the ClusterIP file. Same selector, same ports, one different word on the type line. That's the whole idea. You keep describing the same app and just turn its reach up or down. Apply this on a cloud and the address shows up on its own.

terminal
$ kubectl get svc web-public
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-public LoadBalancer 10.96.31.7 <pending> 80:31544/TCP 8s
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.96.14.201 <none> 80/TCP 3m
web-node NodePort 10.96.22.88 <none> 80:31672/TCP 2m
web-public LoadBalancer 10.96.31.7 203.0.113.24 80:31544/TCP 1m

The first command catches the LoadBalancer mid-setup: EXTERNAL-IP shows <pending> while the cloud builds it, which usually takes a minute or two. List every Service together and the three types line up side by side. ClusterIP has no external address. NodePort has none either, but its PORT column shows 80:31672, the high port Kubernetes chose sitting open on every node. Only LoadBalancer earns a real public address, 203.0.113.24, the one you could point a domain name at.

Which Service type do you need?
Who needs to reach this app?
answer this first, then the type picks itself
only other Pods
ClusterIP
the default; internal address, no public reach
a quick test or bare metal
NodePort
a fixed high port on every node
real users, on a cloud
LoadBalancer
a public address from your cloud provider
Most Services never leave the cluster, so most stay ClusterIP. Got many web apps to publish at once? One shared front door beats giving each its own LoadBalancer.

ClusterIP is internal-only. NodePort opens a high port on every node. LoadBalancer asks the cloud for a public (or internal) balancer. Prefer ClusterIP plus Ingress for HTTP apps rather than a LoadBalancer per microservice — cost and attack surface add up.

NodePort is useful in labs and bare metal; in cloud production it is often an intermediate step under a LoadBalancer. Do not leave wide NodePorts exposed to the internet without a firewall story.

Trade-off: LoadBalancer is easy and expensive at scale; Ingress concentrates TLS and routing. Pick based on protocol and ops maturity, not habit.

Try this

Create ClusterIP and NodePort Services for the same Deployment and compare how they show up in kubectl get svc.

terminal
$ kubectl create deployment web --image=nginx:1.27 --replicas=1
deployment.apps/web created
$ kubectl expose deployment web --name=web-cip --port=80 --type=ClusterIP
service/web-cip exposed
$ kubectl expose deployment web --name=web-np --port=80 --type=NodePort
service/web-np exposed
$ kubectl get svc web-cip web-np
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-cip ClusterIP 10.96.10.5 <none> 80/TCP 6s
web-np NodePort 10.96.10.8 <none> 80:31234/TCP 3s
$ kubectl delete svc web-cip web-np; kubectl delete deployment web
service "web-cip" deleted
service "web-np" deleted
deployment.apps "web" deleted

Takeaway

ClusterIP stays inside; NodePort publishes on nodes; LoadBalancer asks the cloud for an address. Prefer ClusterIP+Ingress for HTTP fleets, and treat every published port as attack surface you must justify.

Don't give every web app its own LoadBalancer
Each LoadBalancer Service is a separate cloud load balancer with its own monthly bill, so ten public web apps means ten bills. The usual fix is to keep the Services as ClusterIP and put a single Ingress in front. An Ingress is one entry point that routes to many internal Services by hostname and path, and handles HTTPS (encrypted web traffic) in one place. You'll meet it in the next lesson.
Quick check
01You're running on a cloud and need to expose one public website to real users. Which Service type is the standard choice?
Incorrect — It only works inside the cluster, so the public can never reach it.
Incorrect — It can reach outside, but the high ports and per-node IPs make it clumsy for real users.
Correct — On a cloud this gives you a real public address built for outside traffic.
Incorrect — Pods get a new IP whenever they're replaced, so anything aimed at a Pod breaks.
02Why does the lesson warn against giving each of ten public web apps its own LoadBalancer Service on a cloud?
Incorrect — It load-balances across all Pods matching its selector, not just one.
Incorrect — That range concerns NodePort, not how many LoadBalancers you create.
Incorrect — You can have many ClusterIP Services; that is not the constraint at play.
Correct — ten LoadBalancers means ten billed cloud resources, so one shared Ingress is the cheaper front door.
03You apply a NodePort Service with nodePort: 8080 and kubectl rejects it: 'provided port is not in the valid range. The range of valid ports is 30000-32767.' What are your options?
Incorrect — NodePort can set a port; it just has to fall within the allowed high range.
Correct — either choose a number in range or omit the field entirely and let Kubernetes assign one.
Incorrect — targetPort is the container's port and has nothing to do with the NodePort range restriction.
Incorrect — 8080 is outside the range and will keep being rejected on every apply.

Related