Kubernetes Services: Exposing Your Applications
Pods come and go — they crash, restart, get replaced during updates, and move between nodes. Every time a pod restarts, it gets a new IP address. If your frontend needs to call your backend, it cannot rely on the backend pod’s IP. Kubernetes Services solve this with a stable network endpoint that follows the pods wherever they go.
What a Service actually is
A Service is a Kubernetes object that defines a stable network endpoint for a set of pods. It does three things:
- Stable IP and DNS name — the Service gets its own cluster IP that never changes, and a DNS name in the form
service-name.namespace.svc.cluster.local. - Load balancing — traffic sent to the Service IP is distributed across all healthy matching pods.
- Service discovery — other pods in the cluster can find the Service by its DNS name without knowing pod IPs.
Under the hood, kube-proxy on each node manages iptables (or IPVS) rules that redirect traffic from Service IPs to actual pod IPs. This all happens transparently.
The four Service types
| Type | Accessible from | Use case | AKS behavior |
|---|---|---|---|
| ClusterIP | Inside the cluster only | Internal service-to-service communication | Assigns a cluster-internal virtual IP |
| NodePort | Node IP + port | External access without a load balancer | Opens a port (30000–32767) on every node |
| LoadBalancer | Public internet | Exposing services to external clients | Provisions an Azure Load Balancer with public IP |
| ExternalName | Inside the cluster | Aliasing an external DNS name | Returns a CNAME, no proxying |
ClusterIP — internal communication
ClusterIP is the default and most common Service type. Use it for any service that only needs to be reachable from within the cluster — backend APIs, databases, caches.
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: production
spec:
selector:
app: api-server
ports:
- port: 80 # Port this Service listens on
targetPort: 8080 # Port the pods listen on
type: ClusterIP # Default, can be omittedOther pods reach this service at http://api-service (within the same namespace) or http://api-service.production.svc.cluster.local (from any namespace).
# Apply the service
kubectl apply -f api-service.yaml
# Inspect the service
kubectl get service api-service
# NAME TYPE CLUSTER-IP PORT(S) AGE
# api-service ClusterIP 10.0.142.200 80/TCP 5m
# Test from inside the cluster
kubectl run test-pod --image=curlimages/curl --rm -it -- \
curl http://api-service/healthLoadBalancer — external access
When you create a LoadBalancer Service in AKS, Azure provisions a public load balancer and assigns a public IP. External clients reach your application through that IP.
apiVersion: v1
kind: Service
metadata:
name: web-frontend
namespace: production
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "false"
spec:
selector:
app: web-frontend
ports:
- port: 443
targetPort: 8443
protocol: TCP
type: LoadBalancerkubectl apply -f web-frontend-service.yaml
# Watch for external IP assignment (takes 1-2 minutes)
kubectl get service web-frontend --watch
# Once external IP is assigned:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# web-frontend LoadBalancer 10.0.88.14 20.50.200.11 443:31204/TCP 2mEach LoadBalancer Service creates one Azure Load Balancer rule. Running many LoadBalancer Services can accumulate significant cost from public IP addresses and load balancer hours. Consider using an Ingress controller to share one load balancer across many services.
Internal load balancer
For services that should only be accessible within your Virtual Network (not from the public internet), use an internal load balancer. Add the annotation:
metadata:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
service.beta.kubernetes.io/azure-load-balancer-internal-subnet: "internal-subnet"This assigns a private IP from your VNet subnet instead of a public IP. Useful for backend APIs that should only be callable from your network, not from the internet.
NodePort — direct node access
NodePort opens a specific port on every node in the cluster and forwards traffic to the Service. External clients connect to any node IP on that port.
apiVersion: v1
kind: Service
metadata:
name: debug-service
spec:
selector:
app: debug-app
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Omit to get a random port in 30000-32767 range
type: NodePortNodePort is rarely used in production because it exposes node IPs directly, does not provide a DNS name or IP that is stable if nodes change, and requires managing firewall rules for each port. It is more useful for development clusters or debugging than for production traffic.
Headless Services
A headless Service has no cluster IP — it returns the individual pod IPs directly in DNS. Set clusterIP: None to create one.
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432Headless Services are used by StatefulSets where clients need to connect to a specific pod (e.g., a specific Postgres primary vs replica) rather than a random pod. DNS for a headless service returns all pod IPs, so clients can connect to individual instances.
Endpoints and endpoint slices
When you create a Service, Kubernetes automatically creates an Endpoints object that tracks the IPs of matching pods. This is what kube-proxy reads to set up routing rules.
# See which pod IPs a service is routing to
kubectl get endpoints web-frontend
# NAME ENDPOINTS AGE
# web-frontend 10.240.0.5:8080,10.240.0.6:8080 5m
# If ENDPOINTS shows <none>, the selector doesn't match any pods
# Check that pod labels match the service selector:
kubectl get pods --show-labels -l app=web-frontendEmpty or missing endpoints are a common cause of connection failures. Always check endpoints when a service is not reachable.
For large clusters with many pods, Kubernetes uses EndpointSlices instead of Endpoints for better scalability. EndpointSlices shard the endpoint list into smaller chunks. In most cases you do not need to interact with EndpointSlices directly.
Service discovery via DNS
Kubernetes runs CoreDNS in the cluster that resolves service names. The naming pattern:
service-name— works within the same namespaceservice-name.namespace— works from any namespaceservice-name.namespace.svc.cluster.local— fully qualified, always works
# Debug DNS resolution from inside a pod
kubectl run dns-test --image=busybox --rm -it -- \
nslookup api-service.production.svc.cluster.local
# Check if CoreDNS is healthy
kubectl get pods -n kube-system -l k8s-app=kube-dns
# View CoreDNS config
kubectl describe configmap coredns -n kube-systemCommon mistakes
- Selector mismatch between Service and Deployment. If the Service selector is
app: webappbut pods are labeledapp: web-app, endpoints will be empty and all traffic will fail. Runkubectl get endpoints service-nameto verify immediately after creating a service. - Creating a LoadBalancer Service for every microservice. Each LoadBalancer Service provisions a separate Azure public IP. Use an Ingress controller with one LoadBalancer to route traffic to multiple services.
- Targeting the container port instead of the service port. The
portin a Service spec is what clients use. ThetargetPortis what the container listens on. These can be different and often are. - Not setting session affinity for stateful HTTP. By default, each request may go to a different pod. If your app requires session stickiness, add
sessionAffinity: ClientIPto the Service spec. - Forgetting that NodePort also creates a ClusterIP. A NodePort Service is also accessible via its ClusterIP from inside the cluster. You do not need a separate ClusterIP service alongside a NodePort.
Summary
- Services give pods a stable IP and DNS name that persists even as individual pods are replaced.
- ClusterIP is internal-only and is the right default for most service-to-service communication.
- LoadBalancer creates an Azure Load Balancer with a public IP. Use internal load balancer annotations for private-only exposure.
- Check
kubectl get endpointsimmediately when a service is not routing traffic — a selector mismatch is the most common cause of empty endpoints. - Use an Ingress controller rather than one LoadBalancer Service per application to reduce cost and simplify routing rules.
Frequently asked questions
Why do I need a Service if pods already have IP addresses?
Pod IPs are ephemeral — they change whenever a pod restarts or is replaced. A Service provides a stable IP and DNS name that stays constant even as the underlying pods change. It also load-balances across multiple pod replicas.
What is the difference between ClusterIP and LoadBalancer?
ClusterIP is internal only — reachable within the cluster. LoadBalancer provisions an Azure Load Balancer with a public IP so external clients can reach the service from the internet.
How does Kubernetes know which pods a Service should send traffic to?
Through label selectors. A Service defines a selector like app=web-app, and sends traffic to all pods with that label. Add or remove pods with the label and they are automatically included or excluded.