Services — ClusterIP, NodePort, LoadBalancer
Pods are ephemeral — they get new IP addresses every time they restart. A Service is a stable network endpoint that abstracts over a dynamic set of pods. It provides a fixed virtual IP (ClusterIP) and DNS name, load-balances traffic across all matching pods, and automatically updates its endpoint list as pods come and go. Services are how everything in Kubernetes talks to everything else.
What Is a Service?
A Service selects pods using a label selector — the same mechanism you use with kubectl get pods -l app=nginx. Any pod with matching labels is automatically included in the Service's endpoint list. Any pod that no longer matches (deleted, label removed, readiness probe failing) is removed.
The four Service types, in order of increasing external accessibility:
| Type | Accessible from | Use case |
|---|---|---|
| ClusterIP | Inside the cluster only | Internal microservice communication |
| NodePort | Outside via any node's IP + port | Development, bare-metal clusters, quick external access |
| LoadBalancer | Outside via a cloud load balancer IP | Production external traffic (cloud providers) |
| ExternalName | DNS alias for an external hostname | Point a cluster-internal name at an external service |
How Services Work
When you create a Service, the control plane assigns it a virtual IP (the ClusterIP) from a reserved range. This IP is not assigned to any real network interface — it only exists in iptables or IPVS rules that kube-proxy programs on every node.
When a pod sends a packet to the ClusterIP, kube-proxy's rules intercept it in the kernel, randomly select one of the Service's healthy endpoints, and rewrite the destination to that pod's real IP. The response comes back through the same NAT path.
ClusterIP
ClusterIP is the default Service type. It creates a virtual IP reachable only from within the cluster. Other pods resolve the Service by its DNS name: <service-name>.<namespace>.svc.cluster.local.
apiVersion: v1
kind: Service
metadata:
name: api
namespace: default
spec:
type: ClusterIP # default — can be omitted
selector:
app: api
ports:
- name: http
port: 80 # port the Service listens on
targetPort: 8080 # port on the pod
DNS resolution from within the same namespace: http://api. From a different namespace: http://api.default.svc.cluster.local.
NodePort
A NodePort Service extends ClusterIP by also opening a port on every node's external IP (range 30000–32767 by default). Traffic to <any-node-ip>:<nodePort> is forwarded to the Service's ClusterIP and then to a pod.
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: NodePort
selector:
app: api
ports:
- port: 80
targetPort: 8080
nodePort: 31080 # optional — auto-assigned if omitted (30000–32767)
NodePort exposes a high-numbered port on every node. Users must know a node's IP. If that node goes down, the entry point is gone. In production, use LoadBalancer or an Ingress controller backed by a load balancer — NodePort is for local clusters, CI environments, or bare-metal setups with an external load balancer you manage yourself.
LoadBalancer
A LoadBalancer Service provisions an external load balancer from your cloud provider (AWS ELB, GCP LB, Azure LB) and assigns it a public IP. Traffic to that IP is forwarded to the Service's NodePort and then to pods. It's the simplest way to expose a service to the internet on cloud Kubernetes.
apiVersion: v1
kind: Service
metadata:
name: api
annotations:
# Cloud-provider-specific annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
type: LoadBalancer
selector:
app: api
ports:
- port: 443
targetPort: 8080
After creation, kubectl get service api shows an EXTERNAL-IP once the cloud provider provisions the load balancer (takes 30–90 seconds).
Each LoadBalancer Service provisions a separate cloud load balancer, which costs money and has its own IP. For hosting multiple HTTP services, use a single Ingress controller backed by one LoadBalancer Service, then route via host/path rules. This is significantly cheaper and more manageable at scale.
ExternalName
An ExternalName Service maps a cluster-internal name to an external DNS hostname. No proxying happens — the service returns a CNAME record. Useful for referencing external services (managed databases, SaaS APIs) by a stable in-cluster name.
apiVersion: v1
kind: Service
metadata:
name: database
namespace: production
spec:
type: ExternalName
externalName: mydb.example.com # external hostname
Pods can then connect to database.production.svc.cluster.local and the DNS resolves to mydb.example.com. Switching from an external database to an in-cluster one only requires changing the Service — no application config changes.
Headless Services
Setting clusterIP: None creates a headless Service — no VIP is assigned. Instead, DNS returns A records for each individual pod IP. This is required for StatefulSets where pods need to be addressed individually.
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # headless
selector:
app: mysql
ports:
- port: 3306
With a headless Service, nslookup mysql.default.svc.cluster.local returns multiple A records — one per pod. mysql-0.mysql.default.svc.cluster.local resolves to the IP of pod mysql-0 specifically.
Selectors & Endpoints
A Service with a selector automatically manages an Endpoints object — a list of pod IPs and ports. You can inspect it:
# See which pods a Service is routing to
kubectl get endpoints api
# NAME ENDPOINTS AGE
# api 10.244.1.5:8080,10.244.2.3:8080 5m
Services without selectors don't auto-manage Endpoints — you manage them manually. This lets you create a Service that routes to an external IP or to pods selected by arbitrary criteria.
kubectl Commands
# Apply a Service
kubectl apply -f service.yaml
# List Services
kubectl get services
kubectl get svc # short form
# Describe a Service (shows endpoints, selector, events)
kubectl describe service api
# Get the ClusterIP
kubectl get service api -o jsonpath='{.spec.clusterIP}'
# Get the external IP of a LoadBalancer Service
kubectl get service api -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# Expose a Deployment quickly (imperative)
kubectl expose deployment api --port=80 --target-port=8080
# Port-forward to a Service for local testing
kubectl port-forward service/api 8080:80
# Delete a Service
kubectl delete service api