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.

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
Note: NodePort is not production-grade for internet-facing traffic. There is no health check at the node level, no TLS termination, and the random port range looks unprofessional in URLs. Use it as a stepping stone or for internal tooling, and prefer LoadBalancer or Ingress for real workloads.

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
Cost tip: Each LoadBalancer service provisions a separate cloud load balancer, which costs money. For HTTP/HTTPS workloads, use a single Ingress controller (backed by one load balancer) and route multiple services through it using host/path rules. See the Ingress guide for details.

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
Note: NetworkPolicies are additive. Once any NetworkPolicy selects a pod, all traffic not explicitly allowed is denied. If no NetworkPolicy selects a pod, all traffic is allowed. Apply a default-deny policy to each namespace and then add explicit allow policies for required communication paths.

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: 80targetPort: 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.