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:

  1. 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.
  2. Load balancing — traffic sent to the Service IP is distributed across all healthy matching pods.
  3. 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

TypeAccessible fromUse caseAKS behavior
ClusterIPInside the cluster onlyInternal service-to-service communicationAssigns a cluster-internal virtual IP
NodePortNode IP + portExternal access without a load balancerOpens a port (30000–32767) on every node
LoadBalancerPublic internetExposing services to external clientsProvisions an Azure Load Balancer with public IP
ExternalNameInside the clusterAliasing an external DNS nameReturns 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 omitted

Other 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/health

LoadBalancer — 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: LoadBalancer
kubectl 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   2m

Each 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: NodePort

NodePort 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: 5432

Headless 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-frontend

Empty or missing endpoints are a common cause of connection failures. Always check endpoints when a service is not reachable.

Note

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 namespace
  • service-name.namespace — works from any namespace
  • service-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-system

Common mistakes

  1. Selector mismatch between Service and Deployment. If the Service selector is app: webapp but pods are labeled app: web-app, endpoints will be empty and all traffic will fail. Run kubectl get endpoints service-name to verify immediately after creating a service.
  2. 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.
  3. Targeting the container port instead of the service port. The port in a Service spec is what clients use. The targetPort is what the container listens on. These can be different and often are.
  4. 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: ClientIP to the Service spec.
  5. 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.

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.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.