Kubernetes Networking in Depth: Pods, ReplicaSets, Deployments, Services, Endpoints, and Ingress

Traffic in Kubernetes is not one magic pipe. Pods get real IPs on the cluster network. ReplicaSets and Deployments keep the right number of those Pods alive. A Service gives clients a stable name and virtual IP in front of moving Pod IPs. Endpoints (or EndpointSlices) are the live list of backends. Ingress adds HTTP routing from outside the cluster. This guide walks the full chain—what each object does, how packets move, and how to debug when something is reachable from kubectl exec but not from your browser.

In short

You declare a Deployment → it owns a ReplicaSet → which creates Pods with labels. A Service selects those labels and gets a cluster IP. The Endpoints controller writes Pod IPs into Endpoints/EndpointSlice. kube-proxy (or a CNI replacement) programs rules so traffic to the Service IP reaches healthy Pods. Ingress plus an ingress controller routes external HTTP(S) to Services. If DNS resolves but connections fail, walk Pod → Endpoints → Service → proxy/CNI → Ingress—in that order.

Why Kubernetes splits networking across objects

Pods are ephemeral: they restart, reschedule, and change IP. Clients cannot hard-code Pod IPs. Kubernetes separates concerns:

  • Workload objects (Deployment, ReplicaSet, Pod) answer “what runs and how many copies?”
  • Service answers “what stable address should callers use for this set of Pods?”
  • Endpoints / EndpointSlice answer “which Pod IPs are healthy backends right now?”
  • Ingress answers “how should HTTP from the internet map to Services?”

That split lets you roll out new Pods without changing Service YAML, and drain bad replicas without touching Ingress host rules—provided labels, selectors, and readiness probes are correct.

For control-plane anatomy see Kubernetes architecture in simple terms. For hands-on apply commands see your first workloads (Part 3). For health gates on endpoints see Kubernetes probes in depth.

Ownership chain: who creates whom

Deployment (you apply)
    └── ReplicaSet (created by Deployment controller)
            └── Pod (created by ReplicaSet controller)

Service (you apply)
    └── Endpoints / EndpointSlice (populated by Endpoints controller from Ready Pods)
            └── Pod IPs + ports (dynamic)

In production you almost never create Pods or ReplicaSets directly. You create a Deployment; controllers create the rest. You create a Service; the Endpoints controller mirrors matching, Ready Pods into backend lists.

Pod: the unit of network identity

A Pod is the smallest object Kubernetes schedules onto a node. For networking, the important facts are:

  • One IP per Pod on the cluster network (assigned by the CNI plugin—Calico, Cilium, Flannel, etc.). Containers in the same Pod share that network namespace: they see localhost for each other’s ports.
  • Pod IP is routable from other Pods in the cluster (unless NetworkPolicy blocks it). It is not stable across Pod restarts.
  • Ports are declared on containers (containerPort is documentation for humans and some systems; the process must actually listen).
  • DNS: Pods get resolvable names like 10-244-1-5.namespace.pod.cluster.local in many clusters; apps usually talk to Services instead.
apiVersion: v1
kind: Pod
metadata:
  name: demo
  labels:
    app: demo
spec:
  containers:
    - name: app
      image: nginx:1.25-alpine
      ports:
        - containerPort: 80

Teaching clusters use bare Pods; production uses Deployments so replacements are automatic.

ReplicaSet: keeping replica count matched

A ReplicaSet ensures a specified number of Pod replicas with matching labels exist. Its spec.selector must match template.metadata.labels. The controller loop:

  1. Count Pods with matching labels.
  2. If too few → create Pods from the template.
  3. If too many → delete Pods (respecting budgets when owned by Deployment).

You rarely apply ReplicaSets by hand. When you scale a Deployment with kubectl scale deployment/web --replicas=5, the Deployment updates its ReplicaSet’s replicas field; the ReplicaSet creates or removes Pods.

During a rolling update, a Deployment may temporarily own two ReplicaSets—old and new—with different pod-template hashes in labels like pod-template-hash=abc123.

Deployment: desired state and rollouts

A Deployment declares desired state for stateless apps: image, env, resources, replica count, rollout strategy. It owns ReplicaSets and drives rolling updates (maxSurge, maxUnavailable, minReadySeconds).

Networking relevance:

  • New Pods must pass readiness before they appear in Service endpoints—so traffic shifts gradually.
  • Old Pods are terminated after new ones are Ready (subject to strategy), reducing black holes during deploys.
  • Label changes on the Pod template create a new ReplicaSet; ensure your Service selector still matches (usually a stable label like app: web, not the hash).
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.25-alpine
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: 80
            periodSeconds: 5

Service: stable front door inside the cluster

A Service is a long-lived API object with a cluster IP (for most types), DNS name <service>.<namespace>.svc.cluster.local, and ports that map to Pod targetPorts. It selects backends by labels (spec.selector), not by Pod name.

Service types

TypeCluster IPTypical useHow external clients reach it
ClusterIP (default) Yes, virtual IP only inside cluster Microservice-to-microservice Ingress, LoadBalancer, or port-forward
NodePort Yes + high port on every node Dev, bare-metal, simple exposure <node-ip>:<nodePort>
LoadBalancer Yes + cloud LB provisioned Public TCP/UDP services on AWS/GCP/Azure Cloud provider external IP/DNS
ExternalName No (CNAME only) Alias to external DNS Redirects DNS to outside hostname
Headless (clusterIP: None) No virtual IP StatefulSet peer discovery, direct Pod DNS DNS returns Pod A records
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
    - name: http
      port: 80        # Service port (what clients connect to)
      targetPort: 80  # Pod port (where container listens)

port and targetPort can differ—e.g. Service port: 80 → Pod targetPort: 8080. Named ports in Pod specs can be referenced by name in targetPort.

What kube-proxy does

On each node, kube-proxy watches Services and Endpoints/EndpointSlices and programs the data plane so traffic to Service.spec.clusterIP:port is load-balanced to backend Pod IPs. Implementations include:

  • iptables — DNAT rules per Service (classic default on many distros).
  • IPVS — better scalability for large numbers of Services.
  • userspace — legacy, rare today.

Modern clusters may use CNI/kube-proxy replacements (e.g. Cilium kube-proxy-free) but the model is the same: Service IP → one of the ready backends.

Session affinity: sessionAffinity: ClientIP sticks a client to one Pod (use carefully with rolling deploys).

Endpoints and EndpointSlice: the live backend list

When you create a Service with a selector, Kubernetes automatically maintains Endpoints (older) or EndpointSlice (scalable, default in modern clusters) objects with the same name as the Service.

Each backend entry is roughly: Pod IP + port. Only Pods that are:

  • Matching the Service selector, and
  • Ready (readiness probe success; startup probe finished if configured)

…appear in the list. Not Ready Pods are omitted—this is how Kubernetes sheds traffic without killing containers.

kubectl get endpoints web -o wide
kubectl get endpointslice -l kubernetes.io/service-name=web

Empty endpoints is the #1 Service debugging signal: wrong labels, no matching Pods, all Pods Not Ready, or selector accidentally removed (selector: {} manual Endpoints pattern).

Services without selectors (external DB, legacy IP) can use manually created Endpoints objects pointing at IPs outside the cluster.

Ingress: HTTP routing from outside

Ingress is an API object describing HTTP/HTTPS rules: host, path, TLS, backend Service. It does not route packets by itself—you need an Ingress controller (nginx, Traefik, AWS Load Balancer Controller, GCE, etc.) that watches Ingress resources and configures a reverse proxy or cloud load balancer.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: nginx
  rules:
    - host: demo.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Typical north-south path:

Internet → cloud LB / NodePort → Ingress controller Pod
         → Service web:80 (cluster IP)
         → kube-proxy/CNI → one of web Pods :80

Ingress vs Service type LoadBalancer: Ingress is L7 (HTTP host/path, TLS termination at controller). LoadBalancer Service is L4 (TCP/UDP) per Service—fine for gRPC, databases, or when you want one LB per Service.

Gateway API (successor in many greenfield designs) expresses similar ideas with more expressive route types; Ingress remains ubiquitous in existing clusters.

DNS: how names resolve

CoreDNS (or kube-dns) runs as a cluster add-on. Pods use /etc/resolv.conf with search paths like default.svc.cluster.local.

  • web in same namespace → Service cluster IP.
  • web.othernsweb.otherns.svc.cluster.local.
  • Headless Service → DNS returns A records for each Pod IP (StatefulSet pattern).

If nslookup web works inside a debug Pod but HTTP fails, the problem is downstream (endpoints, port, NetworkPolicy, app)—not DNS.

CNI and NetworkPolicy (the underlay)

The Container Network Interface (CNI) plugin attaches each Pod to the cluster network, allocates IPs, and may enforce NetworkPolicy (iptables/eBPF rules). Kubernetes Service routing sits on top of Pod connectivity.

Symptom: Pod A cannot reach Pod B IP directly → fix CNI/routing/firewall first. Symptom: Pod A reaches Pod B IP but not Service/web → fix Service, endpoints, or kube-proxy.

For storage and node topics see PVs, PVCs, and StorageClasses and node groups and node pools.

End-to-end flows (east-west and north-south)

Flow A — Pod to Pod via Service (east-west)

  1. App in Pod api resolves web.default.svc.cluster.local → Service cluster IP 10.96.x.x.
  2. Packet destined to 10.96.x.x:80 hits local node’s kube-proxy/CNI rules.
  3. DNAT/load-balance to one Ready Pod IP, e.g. 10.244.2.15:80.
  4. CNI delivers to the node hosting that Pod; container receives on port 80.

Flow B — Internet via Ingress (north-south)

  1. Client hits https://demo.example.com → DNS to cloud load balancer fronting ingress controller.
  2. Controller terminates TLS (if configured), matches Ingress rule, forwards to Service/web:80.
  3. Same Service → Endpoints → Pod path as Flow A.

Flow C — LoadBalancer Service (north-south L4)

  1. Cloud controller sees type: LoadBalancer, provisions ELB/NLB/etc.
  2. External traffic to LB → nodePort/target group → Service cluster IP → Pods.

Complete example: Deployment + Service + Ingress

# web-all.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.25-alpine
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: nginx
  rules:
    - host: demo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
kubectl apply -f web-all.yaml
kubectl get deploy,rs,pod,svc,endpoints,ingress
kubectl describe ingress web

Debugging checklist (ordered)

StepCommand / checkHealthy signal
1. Pods exist kubectl get pods -l app=web Desired replicas Running
2. Ready for traffic kubectl get pods READY column 1/1 or n/n Ready
3. Service selector kubectl describe svc web Selector matches Pod labels
4. Backends listed kubectl get endpoints web Non-empty IP:port list
5. In-cluster reachability Debug Pod: curl http://web HTTP 200 (or expected code)
6. Port mapping Compare port vs targetPort Container actually listens on targetPort
7. NetworkPolicy kubectl get networkpolicy Allows ingress from client namespace/controller
8. Ingress kubectl describe ingress web Address assigned, backend points to correct Service port
# Quick in-cluster probe
kubectl run curl --rm -it --image=curlimages/curl --restart=Never -- \
  curl -s -o /dev/null -w "%{http_code}\n" http://web.default.svc/

# Port-forward bypasses Ingress/LB (isolates Service → Pod)
kubectl port-forward svc/web 8080:80

Deeper incident patterns: Kubernetes troubleshooting playbook.

Common mistakes

  • Service selector typoapp:web vs app: web; endpoints stay empty.
  • Targeting wrong port — Service port: 80 but app listens on 8080 without fixing targetPort.
  • Readiness never passes — Pods Running but Not Ready; Ingress/LB see zero backends.
  • Using Pod IP in config — breaks on every restart; use Service DNS.
  • Ingress without controller — Ingress object exists, no implementation, no ADDRESS.
  • mixing pod-template-hash into Service selector — breaks during rollouts when hash changes.
  • assuming ClusterIP is reachable from laptop — use port-forward, Ingress, or LB.

Production checklist

  • Stable labels on Pod template; Service selector uses those labels only.
  • Readiness probes gate Endpoints; liveness separate (probes guide).
  • Named ports where multiple ports exist; document port vs targetPort.
  • Ingress TLS via cert-manager or cloud-managed certs; redirect HTTP → HTTPS.
  • NetworkPolicies: default-deny with explicit allow for ingress controller and monitoring.
  • Headless Services only when you need per-Pod DNS (StatefulSets).
  • Monitor endpoint count; alert when endpoints = 0 for critical Services.

Object reference (one glance)

ObjectPrimary jobNetworking role
PodRun containersReal IP; processes listen on ports
ReplicaSetHold replica countIndirect—creates Pods
DeploymentRollouts + desired stateIndirect—controls which Pods exist
ServiceStable cluster IP + DNSVirtual front door; kube-proxy programs paths
Endpoints / EndpointSliceBackend listPod IP:port for Ready replicas
IngressHTTP routing rulesNorth-south L7 to Services (with controller)

Further reading

  • Kubernetes documentation — Services, Ingress, EndpointSlices, DNS for Services and Pods
  • Hands-on Part 3 — apply Deployment and Service on a local cluster
  • Probes in depth — how readiness controls endpoints
  • HPA in depth — scaling replicas behind the same Service
  • Cluster RBAC — who can expose Services and Ingress

Blog index · Architecture · Hands-on Part 3 · Probes · Troubleshooting

Back to blog list