Kubernetes Services and Networking: ClusterIP, NodePort, LoadBalancer (2026)
Pods are ephemeral — they come and go, and their IP addresses change every time they restart. Kubernetes Services solve this by providing a stable virtual IP and DNS name that front a dynamic set of pods. Understanding the four service types, how kube-proxy programs the network, and how CoreDNS enables service discovery is essential for building reliable microservice architectures.
Table of Contents
ClusterIP — Internal Services
ClusterIP is the default service type. It creates a virtual IP address that is only reachable from within the cluster. Traffic to the ClusterIP is load-balanced across all healthy pods matching the selector.
apiVersion: v1
kind: Service
metadata:
name: api-server
namespace: production
labels:
app: api-server
spec:
type: ClusterIP # default; can be omitted
selector:
app: api-server # routes to pods with this label
ports:
- name: http
port: 80 # port the Service listens on
targetPort: 8080 # port the container listens on
protocol: TCP
- name: metrics
port: 9090
targetPort: 9090
# Inspect the service
kubectl get svc api-server -n production
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# api-server ClusterIP 10.100.45.231 <none> 80/TCP,9090/TCP 3d
# Test connectivity from inside the cluster
kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- \
curl http://api-server.production.svc.cluster.local/health
NodePort — External Access on Node
NodePort extends ClusterIP by additionally opening a port (30000–32767) on every node in the cluster. External traffic can reach pods by connecting to <any-node-ip>:<nodePort>. This is mainly useful for development and on-premises clusters without a cloud load balancer.
apiVersion: v1
kind: Service
metadata:
name: api-server-np
namespace: production
spec:
type: NodePort
selector:
app: api-server
ports:
- port: 80
targetPort: 8080
nodePort: 31080 # omit to let Kubernetes assign a port in 30000-32767
# Access via any node's IP
curl http://<node-external-ip>:31080/health
LoadBalancer — Cloud Load Balancers
The LoadBalancer type provisions an external load balancer from your cloud provider (AWS NLB/ALB, GCP Cloud LB, Azure LB) and assigns a public IP to the service. It is a superset of NodePort — a ClusterIP and NodePort are also created automatically.
apiVersion: v1
kind: Service
metadata:
name: api-server-lb
namespace: production
annotations:
# AWS: use NLB instead of classic ELB
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
type: LoadBalancer
selector:
app: api-server
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
# Optional: restrict source IP ranges
loadBalancerSourceRanges:
- "10.0.0.0/8"
- "203.0.113.0/24"
kubectl get svc api-server-lb -n production
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# api-server-lb LoadBalancer 10.100.45.232 203.0.113.100 80:31234/TCP 5m
ExternalName and Headless Services
ExternalName
ExternalName maps a Service to an external DNS name rather than a set of pods. When a pod queries the service name, CoreDNS returns a CNAME record pointing to the external hostname. Useful for integrating external databases or third-party APIs without hardcoding URLs in application config.
apiVersion: v1
kind: Service
metadata:
name: external-db
namespace: production
spec:
type: ExternalName
externalName: mydb.us-east-1.rds.amazonaws.com
# Inside the cluster, pods connect to:
# external-db.production.svc.cluster.local
# which resolves to mydb.us-east-1.rds.amazonaws.com
Headless Services
Setting clusterIP: None creates a headless service — no virtual IP is allocated. Instead, DNS returns the individual pod IPs directly. Essential for StatefulSets where each pod needs a stable, individually addressable DNS name.
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: production
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
# With StatefulSet named 'postgres' and headless service 'postgres-headless':
# Pod DNS names:
# postgres-0.postgres-headless.production.svc.cluster.local
# postgres-1.postgres-headless.production.svc.cluster.local
# postgres-2.postgres-headless.production.svc.cluster.local
Endpoints and EndpointSlices
When you create a Service with a selector, Kubernetes automatically creates and manages an Endpoints (or EndpointSlice in 1.21+) object that tracks the IPs of matching healthy pods. You can also create a Service without a selector and manage Endpoints manually — useful for pointing a Kubernetes service at an external IP or an on-premises server.
# Service without selector
apiVersion: v1
kind: Service
metadata:
name: legacy-db
namespace: production
spec:
ports:
- port: 5432
targetPort: 5432
---
# Manual Endpoints
apiVersion: v1
kind: Endpoints
metadata:
name: legacy-db # must match Service name
namespace: production
subsets:
- addresses:
- ip: 192.168.1.50 # on-premises database IP
ports:
- port: 5432
# Inspect endpoints for a service
kubectl get endpoints api-server -n production
# NAME ENDPOINTS AGE
# api-server 10.244.1.5:8080,10.244.2.7:8080,10.244.3.3:8080 3d
kube-proxy: iptables vs ipvs
kube-proxy runs on every node and programs the node's network rules to implement Service load balancing. It watches the API server for Service and Endpoints changes and updates rules accordingly.
iptables mode (default): Programs netfilter iptables rules with DNAT to rewrite the destination IP from the Service ClusterIP to a randomly selected pod IP. Simple and battle-tested, but performance degrades with thousands of services due to linear rule traversal.
ipvs mode: Uses the Linux IPVS (IP Virtual Server) kernel module. It stores rules in a hash table, giving O(1) lookup regardless of the number of services. Supports multiple load-balancing algorithms (round-robin, least connections, source IP hash). Recommended for clusters with 1000+ services.
# Check current proxy mode
kubectl -n kube-system get configmap kube-proxy -o yaml | grep mode
# Enable ipvs mode in kube-proxy ConfigMap
kubectl -n kube-system edit configmap kube-proxy
# Set: mode: "ipvs"
# Then restart kube-proxy pods:
kubectl -n kube-system rollout restart daemonset kube-proxy
CoreDNS and Service Discovery
CoreDNS runs as a Deployment in kube-system and serves DNS for the entire cluster. Every pod gets /etc/resolv.conf configured to use the CoreDNS ClusterIP, so DNS queries for service names are automatically resolved.
The DNS naming pattern for Services:
# Full DNS name:
<service-name>.<namespace>.svc.cluster.local
# Examples:
api-server.production.svc.cluster.local # → ClusterIP
postgres-0.postgres-headless.production.svc.cluster.local # → pod IP (headless)
# Within the same namespace, you can use short names:
curl http://api-server/health # resolves in same namespace
curl http://api-server.production/health # cross-namespace
# Check CoreDNS pods
kubectl -n kube-system get pods -l k8s-app=kube-dns
# Debug DNS from inside a pod
kubectl run dnsutils --image=gcr.io/kubernetes-e2e-test-images/dnsutils:1.3 \
--rm -it --restart=Never -- nslookup api-server.production.svc.cluster.local
NetworkPolicy Basics
By default all pods in a cluster can communicate with all other pods. NetworkPolicy objects restrict ingress and egress traffic at the pod level — implemented by your CNI plugin (Calico, Cilium, Weave). Not all CNI plugins support NetworkPolicy; Flannel does not by default.
# Allow only the frontend to call the api-server on port 8080
# Deny all other ingress to api-server pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-server-ingress
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
# Allow calls to postgres
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
Frequently Asked Questions
Why does my Service show EXTERNAL-IP as <pending>?
A LoadBalancer service is pending when no cloud controller manager is available to provision the external load balancer. This happens when running on bare metal, minikube, or kind without a load balancer implementation. Solutions: use MetalLB for bare metal clusters, or switch to NodePort for local development. On cloud clusters, check that the cloud controller manager pod is running and has the correct IAM permissions.
What is the difference between port, targetPort, and nodePort in a Service?
port is the port the Service itself listens on (what other pods use to connect). targetPort is the port the traffic is forwarded to on the pod container. nodePort (NodePort/LoadBalancer types only) is the port opened on every cluster node for external access. A typical mapping: port: 80 → targetPort: 8080 with nodePort: 31080.
How does session affinity work in Kubernetes Services?
Set spec.sessionAffinity: ClientIP to route all requests from the same client IP to the same pod. You can configure the timeout with sessionAffinityConfig.clientIP.timeoutSeconds (default 10800 = 3 hours). Note this only works reliably when the client IP is preserved; behind a load balancer you may need to use cookie-based affinity at the Ingress layer instead.
Can I access a service in another namespace?
Yes, using the full DNS name: <service>.<namespace>.svc.cluster.local. For example, to call the api-server service in the production namespace from the monitoring namespace: http://api-server.production.svc.cluster.local. Make sure NetworkPolicy allows the cross-namespace traffic if you have default-deny policies.
What CNI plugin should I use for NetworkPolicy support?
Calico and Cilium are the two most widely deployed CNI plugins with full NetworkPolicy support. Cilium also provides eBPF-based networking for superior performance and observability (Hubble). Calico is simpler to operate and has a large production track record. Flannel is popular for simplicity but does not enforce NetworkPolicies natively — if you need network security, choose Calico or Cilium.