On this page
In the previous article you built a ClusterIP Service and reached it from another Pod. That covers traffic inside the cluster. It does nothing for the last of the four networking problems: letting a real user, sitting outside the cluster, reach your application.
This article covers the Service types that solve that, plus two special cases that often confuse people. There is only one kind: Service in Kubernetes; the type field is what changes its behaviour.
We will cover the following topics:
- NodePort, and the three different ports in its specification
- Why
kubectl port-forwardis not a substitute for it - LoadBalancer, and when it is worth using
- Headless Services, which are ClusterIP with the load balancing removed
- ExternalName, which points at something outside the cluster entirely
Technical Requirements
- A working Kubernetes cluster and
kubectl. - Docker and the .NET 10 SDK if you want to build the sample image.
- For the LoadBalancer section, a cloud cluster or a local cluster with a load balancer controller. The article explains what to expect if you have neither.
Core Concepts
Choosing a Service Type
The types are not alternatives to each other. Each answers a different question, so the choice is usually obvious once the question is clear.
| Type | Reachable from | Use it when |
|---|---|---|
| ClusterIP | Inside the cluster only | One Pod needs to call another. The default. |
| NodePort | Any node's IP, on a high port | You need external access without a cloud load balancer. |
| LoadBalancer | A dedicated external IP | You are on a cloud that provisions load balancers for you. |
| ExternalName | Not applicable | You want a cluster name for something outside the cluster. |
One Service can only be one type. But nothing stops you from creating two Services with different names and different types over the same Pods, which is a common pattern for an application that must be reachable both internally and externally.
The first three types nest. A NodePort Service also gets a ClusterIP. A LoadBalancer Service also gets a NodePort and a ClusterIP. Each one is a superset of the one above it.
NodePort and Its Three Ports
A NodePort Service opens the same port on every node in the cluster. Sending a request to any node's IP on that port reaches your Pods, no matter which node they actually run on. The address is <any-node-ip>:<nodePort>.
The part that trips people up is that its ports block names three ports:
| Field | Lives on | Meaning |
|---|---|---|
nodePort | The worker node | The externally reachable port. Must be in the 30000-32767 range by default. Omit it and Kubernetes picks one for you. |
port | The Service | The port the Service itself listens on, used by cluster-internal callers. |
targetPort | The Pod | The port the container is actually listening on. |
Read it as a chain: a request arrives at nodePort on a node, is handled by the Service on its port, and is forwarded to targetPort on a selected Pod.
Is NodePort Production Ready?
It is a real feature, not a debugging tool, and it does not need kubectl to keep running. But it has two limitations you must plan around.
The port range. You are restricted to 30000-32767. Browsers expect web traffic on 80 and 443. Nobody is going to type a port number after your domain name.
It is only one layer. A NodePort points at node IPs. If a node fails, that address is gone, so something in front must know which nodes are healthy.
The usual answer is a tiered architecture: put a reverse proxy or a cloud load balancer in front of the cluster, listening on 80 and 443, and have it forward to the NodePort on the healthy nodes. Kubernetes also offers Ingress and an Ingress controller, which let you configure a reverse proxy such as nginx or HAProxy through Kubernetes objects. That is a topic of its own.
NodePort Is Not kubectl port-forward
These two feel similar because both let you open a browser and see your application, but they are for completely different purposes.
kubectl port-forward | NodePort | |
| Lifetime | Only while the command runs. Ctrl+C ends it. | Permanent, until the Service is deleted. |
| Who can reach it | Only you, on your machine | Anyone who can reach a node |
| Needs kubectl | Yes, plus cluster credentials | No |
| Intended for | Debugging and testing | Real traffic |
Use port-forward to test. Use NodePort to expose. Do not mix the two up: a port-forward is not a deployment strategy, and creating NodePort Services for every quick check leaves clutter behind.
LoadBalancer
A LoadBalancer Service asks the infrastructure to provision a real load balancer with its own external IP address and point it at your Pods. Provisioning is asynchronous; the result appears in the Service's .status.loadBalancer field and in the EXTERNAL-IP column, which shows <pending> until it is ready.
This requires infrastructure that knows how to do it. AWS, Google Cloud, Azure, and OpenStack all support it. On a bare-metal cluster with nothing installed, the EXTERNAL-IP stays <pending> forever, though the NodePort that comes with it still works. Projects such as MetalLB fill that gap.
Two optional fields are worth knowing:
loadBalancerIPrequests a specific address. Providers that do not support it ignore the field silently.loadBalancerClassselects a non-default implementation. It can only be set on a LoadBalancer Service, and it cannot be changed afterwards.
Many teams deliberately avoid this type. The Kubernetes object exposes only a generic subset of what a cloud load balancer can do, so anything sophisticated ends up configured outside Kubernetes anyway. And each LoadBalancer Service typically bills as a separate cloud resource, so twenty Services means twenty load balancers. One Ingress in front of many ClusterIP Services is usually cheaper and more flexible.
Headless Services
A headless Service is not a fourth type. It is a ClusterIP Service with clusterIP: None, and that one line changes everything about how it behaves.
A normal ClusterIP Service gives you one virtual address and proxies your traffic. A headless Service allocates no address at all. DNS returns the addresses of the Pods themselves, and the client decides which to use.
So the proxying and the load balancing are gone. There is still a crude form of balancing, because DNS returns the records in varying order, but that happens in the resolver, not in Kubernetes. Responsibility moves to the client.
This is what you want when Pods are not interchangeable. Clustered stateful systems are the classic case: an LDAP client, a database driver that needs to know which node is the primary, or a sharded store that maps shard numbers to specific replicas. All of those need to address individual Pods, which a load-balanced virtual IP actively prevents.
ExternalName
An ExternalName Service has no selector, no ports, and no Pods. It is purely a DNS alias: cluster DNS answers with a CNAME record pointing at a name outside the cluster.
Your application asks for partner-crm and ends up talking to crm.partner.example.com, without knowing anything about it. The redirection happens entirely at the DNS layer, and no traffic passes through Kubernetes.
This buys you three things:
- Simpler configuration. Applications only ever know a service name.
- Freedom to move. If you later migrate that database into the cluster, you replace the ExternalName Service with a normal one and nothing else changes.
- Fewer leaked details. External hostnames live in one object rather than scattered across application configuration.
Hands-On: Kubernetes Commands
List Services with their types and ports
The PORT(S) column shows port:nodePort for NodePort Services.
Find the node IP addresses
You need one of these to reach a NodePort Service.
On minikube, get the URL directly
Saves you from assembling the address by hand.
Watch a LoadBalancer being provisioned
EXTERNAL-IP shows <pending> until the provider responds.
See all DNS records behind a headless Service
dig +short lists every A record, which is the point of a headless Service.
Confirm an ExternalName is a CNAME
The answer is a name, not an address.
Delete a Service
This removes network access but leaves every Pod running. Pods and Services have entirely separate life cycles.
Step-by-Step Example
The Scenario
A kiosk API with three replicas in a storefront namespace. Each reply names the Pod and the node that produced it, which makes the routing behaviour of each Service type visible. We expose the same three Pods four different ways.
Step 1 — Namespace and Application
Create the application with the .NET CLI:
Replace Program.cs:
Add a Dockerfile and build:
Deploy three replicas:
Step 2 — The NodePort Service
Three ports, one Service. The comments name which machine each one lives on.
Read 80:30380/TCP as "Service port 80, node port 30380". Note that it also has a ClusterIP, so cluster-internal callers can still use the name and port 80 as before.
Step 3 — Prove the Port Is Open on Every Node
First list the nodes:
Now hit the same port on each of them in turn:
Two things are worth noticing. Every node answers, including the control plane, which runs no application Pods at all. And a request to desktop-worker was served by a Pod on desktop-worker2. The node you contact and the node that runs the Pod are unrelated; kube-proxy forwards across the cluster network.
A practical note: on some local setups the node addresses live on an internal Docker network that your host cannot reach. If curl from your laptop times out, run the loop from a Pod inside the cluster, or use minikube service --url. The NodePort itself is working either way.
Step 4 — The LoadBalancer Service
The specification is shorter, because the infrastructure fills in the details.
Three things were allocated at once: a ClusterIP, a node port (31596, chosen automatically because we did not ask for one), and an external IP. That is the nesting described earlier, visible in a single line of output.
If your EXTERNAL-IP stays <pending>, your cluster has no load balancer provider. That is normal on plain bare metal. The Service still works through its node port in the meantime.
Step 5 — The Headless Service
One line separates this from an ordinary ClusterIP Service.
Now compare what DNS returns for each of the two Services:
The headless Service returned three Pod addresses. The ordinary Service returned one virtual address. Compare the first three against the actual Pods:
They match exactly. The headless Service handed the client the real Pod addresses and got out of the way. There is no proxy in the path any more, which is precisely what a clustered database client needs and precisely what a plain web client does not.
Step 6 — The ExternalName Service
No selector, no ports, no Pods. Just a name.
CLUSTER-IP and PORT(S) are both empty, which is unlike every other Service you have seen. Confirm what it really is:
A CNAME, nothing more. An application configured to call partner-crm now reaches the partner system, and the day that system moves into your cluster you replace this object with a normal Service and change no application configuration at all.
Step 7 — Four Services, One Set of Pods
The first three all select the same three Pods and expose them in three different ways at the same time. This is a normal thing to do, not a trick.
Step 8 — Deleting a Service Does Not Delete Pods
All three Pods are still running. Only that route to them is gone. Treat Service deletion with care: it is invisible from the cluster's point of view and immediately visible to your users.
Step 9 — Clean Up
Summary
- There is one
kind: Service. Thetypefield decides its behaviour, and a Service can only be one type at a time. - The types nest: LoadBalancer includes a NodePort, which includes a ClusterIP.
- NodePort opens one port on every node, in the 30000-32767 range. The node you contact need not run the Pod that answers.
- Its three ports are
nodePorton the node,porton the Service, andtargetPorton the Pod. kubectl port-forwardis a debugging tool that dies with the command. NodePort is a permanent route.- LoadBalancer needs infrastructure support and bills per Service. One Ingress in front of many ClusterIP Services is usually cheaper.
- Headless is
clusterIP: None. DNS returns the Pod addresses and the client chooses. Use it when Pods are not interchangeable. - ExternalName is a CNAME to something outside the cluster, with no selector, no ports, and no traffic passing through Kubernetes.
- Deleting a Service never deletes Pods. Their life cycles are independent.
In the final article of this series we look at readiness, liveness, and startup probes, which decide when a Pod is allowed to appear behind any of these Services.