The kinds of Service
ClusterIP, NodePort, 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.
apiVersion: v1kind: Servicemetadata:name: webspec:type: ClusterIPselector:app: webports:- port: 80targetPort: 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.
$ kubectl apply -f svc-clusterip.yamlservice/web created$ kubectl get svc webNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb 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.
$ kubectl get endpoints webNAME ENDPOINTS AGEweb 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.
apiVersion: v1kind: Servicemetadata:name: web-nodespec:type: NodePortselector:app: webports:- port: 80targetPort: 8080nodePort: 8080
$ kubectl apply -f svc-nodeport.yamlThe 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.
$ kubectl apply -f svc-nodeport.yamlservice/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.
apiVersion: v1kind: Servicemetadata:name: web-publicspec:type: LoadBalancerselector:app: webports:- port: 80targetPort: 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.
$ kubectl get svc web-publicNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb-public LoadBalancer 10.96.31.7 <pending> 80:31544/TCP 8s$ kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb ClusterIP 10.96.14.201 <none> 80/TCP 3mweb-node NodePort 10.96.22.88 <none> 80:31672/TCP 2mweb-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.
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.
$ kubectl create deployment web --image=nginx:1.27 --replicas=1deployment.apps/web created$ kubectl expose deployment web --name=web-cip --port=80 --type=ClusterIPservice/web-cip exposed$ kubectl expose deployment web --name=web-np --port=80 --type=NodePortservice/web-np exposed$ kubectl get svc web-cip web-npNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb-cip ClusterIP 10.96.10.5 <none> 80/TCP 6sweb-np NodePort 10.96.10.8 <none> 80:31234/TCP 3s$ kubectl delete svc web-cip web-np; kubectl delete deployment webservice "web-cip" deletedservice "web-np" deleteddeployment.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.