Pods come and go, and every new Pod gets a new IP address. A Service gives a group of Pods one stable virtual IP and DNS name, and spreads connections across whichever Pods are ready at that moment. The Kubernetes service types decide who can reach that address: ClusterIP for traffic inside the cluster, NodePort for a fixed port on every node, LoadBalancer for an external load balancer, and ExternalName for a DNS alias to something outside the cluster.
This guide explains each type, the headless Service that StatefulSets depend on, and what kube-proxy and external load balancers actually do on the path of a request.
How a Kubernetes Service works
A Service selects Pods by label. Three pieces work together:
- The EndpointSlice controller keeps a list of the IPs of matching Pods that pass their readiness probe.
- kube-proxy on every node turns that list into iptables, IPVS or nftables rules that send traffic for the Service IP to one of those Pods.
- CoreDNS gives the Service a name:
orders.shop.svc.cluster.local, or justordersfrom inside theshopnamespace.
apiVersion: v1
kind: Service
metadata:
name: orders
namespace: shop
spec:
type: ClusterIP # the default, can be omitted
selector:
app: orders
ports:
- name: http
port: 80 # the port clients use on the Service IP
targetPort: http # the named containerPort on the Pod (e.g. 8080)Keep the ports straight. port is what clients connect to on the Service. targetPort is where the container listens, and naming it lets you change the container port without touching the Service. nodePort appears only on NodePort and LoadBalancer Services.
ClusterIP: internal service-to-service traffic
ClusterIP is the default type. The Service gets a virtual IP from the cluster's service range, reachable only from inside the cluster. Use it for everything that doesn't need outside access: backend to database, API to cache, one microservice to another.
The virtual IP isn't bound to any real interface. It only exists as forwarding rules, which is why you usually can't ping it even when the Service works fine. Test it with a real request instead:
kubectl run -it --rm curl --image=curlimages/curl --restart=Never -- \
curl -s http://orders.shop/healthzTwo behaviors surprise people:
- Load balancing happens per connection, not per request. kube-proxy works at L4. A client holding one long-lived HTTP/2 or gRPC connection sends every request to the same Pod, so traffic can be very uneven. Client-side balancing or a service mesh fixes this. The article on resilient microservice communication covers the patterns.
- Only ready Pods receive traffic. If the readiness probe fails, the Pod drops out of the EndpointSlice, and when none are ready the Service refuses connections.
sessionAffinity: ClientIP pins each client IP to one Pod if you need sticky sessions at L4.
NodePort: a fixed port on every node
A NodePort Service is a ClusterIP Service plus a port, by default in the range 30000–32767, opened on every node. Traffic to <any-node-IP>:<nodePort> reaches the Service, even on nodes that don't run any of its Pods.
apiVersion: v1
kind: Service
metadata:
name: orders-nodeport
namespace: shop
spec:
type: NodePort
selector:
app: orders
ports:
- port: 80
targetPort: http
nodePort: 30080 # optional; omitted means auto-assignedThe downsides are why NodePort is rarely exposed directly to users:
- High, unusual port numbers.
- Clients need to know node IPs and must stop using nodes that go down.
- Nothing in front spreads traffic across nodes or checks their health.
NodePort is useful for quick tests, for bare-metal setups where you run your own load balancer in front, and as the building block for LoadBalancer.
LoadBalancer: exposing a Service outside the cluster
A LoadBalancer Service builds on NodePort. The cloud-controller-manager asks your cloud for a load balancer, points it at the node ports (some clouds can target Pod IPs directly), and writes its address into the Service status:
apiVersion: v1
kind: Service
metadata:
name: orders-public
namespace: shop
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: orders
ports:
- port: 443
targetPort: httpskubectl get svc orders-public -n shop
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# orders-public LoadBalancer 10.96.41.12 203.0.113.25 443:31544/TCPexternalTrafficPolicy controls what happens once traffic reaches a node:
| Policy | Behavior | Trade-off |
|---|---|---|
Cluster (default) | Any node accepts traffic and forwards it to a Pod, possibly on another node | Extra hop, and the Pod sees the node's IP instead of the client's |
Local | Nodes only forward to Pods on the same node. Nodes without one fail the LB health check | Preserves the client IP and avoids the hop, but load can be uneven |
Each LoadBalancer Service usually gets its own load balancer and IP, and that gets expensive when you have many services. For HTTP traffic, put one load balancer in front of an Ingress controller and route by host and path, as described in Kubernetes Ingress routing and TLS.
Internal vs external load balancers
By default the load balancer is internet-facing. To expose a Service only to your private network, such as other VPCs or an office VPN, request an internal load balancer through a provider-specific annotation:
metadata:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true" # AKS
# networking.gke.io/load-balancer-type: "Internal" # GKECheck your provider's documentation for the exact annotation. spec.loadBalancerSourceRanges restricts which client CIDRs are allowed, on providers that support it.
LoadBalancer on bare metal with MetalLB
Without a cloud provider, nothing fulfils the request and EXTERNAL-IP stays <pending>. MetalLB fills the gap with two components that run inside the cluster:
- A controller that assigns IPs from a pool you define.
- A speaker DaemonSet that announces those IPs to your network, using ARP in Layer 2 mode or BGP to your routers.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public
namespace: metallb-system
spec:
addresses:
- 203.0.113.10-203.0.113.20
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: public
namespace: metallb-system
spec:
ipAddressPools:
- publicDon't confuse this with kube-proxy. The external load balancer, or MetalLB's speaker, gets traffic from outside to a node. kube-proxy's rules then pick a Pod. Both balance load, at different points on the path. Multi-layer load balancing looks at the full chain from DNS to Pod.
ExternalName: a DNS alias
An ExternalName Service has no selector, no virtual IP and no proxying. CoreDNS returns a CNAME record:
apiVersion: v1
kind: Service
metadata:
name: orders-db
namespace: shop
spec:
type: ExternalName
externalName: orders.abc123.ap-southeast-1.rds.amazonaws.comApplications connect to orders-db, and you can later move the database into the cluster by replacing this with a normal Service, without changing application config. One caveat: clients still send the name they were given, so HTTP Host headers and TLS certificate checks see orders-db, not the external hostname. It works best for protocols that don't validate hostnames, or when the client can be configured for the real name.
Headless Services and DNS for StatefulSets
Setting clusterIP: None makes a Service headless. There's no virtual IP and kube-proxy ignores it. Instead, a DNS query for the Service returns the IP of every ready Pod:
apiVersion: v1
kind: Service
metadata:
name: kafka-headless
namespace: data
spec:
clusterIP: None
selector:
app: kafka
ports:
- name: broker
port: 9092$ nslookup kafka-headless.data.svc.cluster.local
Name: kafka-headless.data.svc.cluster.local
Address: 10.244.1.17
Name: kafka-headless.data.svc.cluster.local
Address: 10.244.2.9
Name: kafka-headless.data.svc.cluster.local
Address: 10.244.3.22A normal ClusterIP hides which Pod you reach. A headless Service lets the client see every Pod and choose. Combined with a StatefulSet whose serviceName points at it, each Pod also gets its own stable DNS name:
kafka-0.kafka-headless.data.svc.cluster.local
kafka-1.kafka-headless.data.svc.cluster.local
kafka-2.kafka-headless.data.svc.cluster.localThis is how Kafka brokers, database replicas and etcd members find and address each other. If peers need to discover each other before they pass readiness, set publishNotReadyAddresses: true. The StatefulSet side is covered in Kubernetes workloads.
Kubernetes service types compared
| Type | Virtual IP | Reachable from | Typical use |
|---|---|---|---|
| ClusterIP | Yes | Inside the cluster | Service-to-service calls |
| NodePort | Yes | <nodeIP>:30000–32767 | Tests, bare metal behind your own LB |
| LoadBalancer | Yes | External or internal LB address | Exposing TCP/UDP services, Ingress controllers |
| ExternalName | No | Inside the cluster (DNS only) | Aliasing an external host |
| Headless | No | Inside the cluster (per-Pod DNS) | StatefulSets, client-side discovery |
Debugging a Service that doesn't respond
- Check the endpoints:
kubectl get endpointslices -n shop -l kubernetes.io/service-name=orders. If it's empty, the selector doesn't match or no Pod is ready. - Compare labels:
kubectl get pods -n shop -l app=orders --show-labels. - Confirm
targetPortmatches the port the container actually listens on. - Test DNS and HTTP from a temporary Pod in the same namespace.
- Look for a NetworkPolicy that blocks the traffic.
- For
EXTERNAL-IP <pending>, check that a cloud controller or MetalLB is running and has free IPs.
FAQ
What is the default Kubernetes Service type?
ClusterIP. If you omit type, the Service is reachable only from inside the cluster.
What is the difference between NodePort and LoadBalancer?
NodePort opens a port on every node, and clients must reach the nodes themselves. LoadBalancer adds an external or internal load balancer in front of those node ports, with a single stable address and health checks.
Why can't I ping a ClusterIP?
The ClusterIP exists only as forwarding rules for the Service's ports, not as a real interface, so ICMP usually gets no reply. Test with the actual protocol, such as curl or nc.
When should I use a headless Service?
When clients need to reach specific Pods rather than any Pod, typically members of a StatefulSet such as Kafka brokers or database replicas.
Do I need a LoadBalancer Service or an Ingress?
For HTTP and HTTPS, usually one LoadBalancer for the Ingress controller and Ingress rules for each app. For raw TCP or UDP, such as a database or MQTT, use a LoadBalancer Service.
Choosing a Service type
- Start with ClusterIP. Most Services never need anything else.
- Expose HTTP through Ingress, not one LoadBalancer per app.
- Use LoadBalancer for non-HTTP protocols, and use an internal load balancer when only your private network needs access.
- Use headless Services for StatefulSets and client-side discovery.
- When something breaks, check the EndpointSlice first.
Vectorkub's free DevOps courses let you try each Service type on a real cluster.
