Chapter 25 of 47 · Networking, part 2

Kubernetes networking

With the basics from chapter 24, Kubernetes networking becomes four simple questions: how do containers in a Pod talk, how do Pods talk, how do Pods reach a Service, and how does the outside world get in. Each answer reuses an idea you already know.

The four kinds of traffic

Click each numbered path:

Four kinds of traffic in a cluster1: two containers in the same Pod use localhost. 2: a Pod on node 1 talks to a Pod on node 2 by Pod IP. 3: a Pod calls a Service, which forwards to a Pod. 4: a user outside reaches the cluster through an Ingress or LoadBalancer. cluster node-1 · Pods 10.244.1.0/24 node-2 · Pods 10.244.2.0/24 Pod posts10.244.1.15 posts sidecar Pod comments10.244.2.7 comments-srvClusterIP 10.96.31.40 Userinternet 1 2 3 4

2 · Pod to Pod

Every Pod has a real IP from the Pod range, reachable from any node. Basic ideas: IP ranges and routing (#4, #6).

The three rules

Kubernetes itself does not wire up the network. It sets rules, and a CNI plugin (Calico, Cilium, Flannel, or your cloud's own) builds a network that follows them:

  1. Every Pod gets its own IP address.
  2. Every Pod can reach every other Pod by that IP, on any node, with no NAT: the address a Pod sees for itself is the address others use.
  3. Nodes can reach every Pod, and Pods can reach nodes.

So the whole cluster feels like one big flat network. No port mapping between Pods, unlike docker run -p.

1 · Container to container: localhost

Covered in chapter 6: one Pod is one network namespace, so its containers share one IP and one localhost, and must use different ports.

sidecar → http://localhost:4000 → posts   (never leaves the Pod)

2 · Pod to Pod: a flat network

Each node gets a slice of the Pod range for its Pods. Step through a request from posts on node 1 to comments on node 2:

  1. node-1 owns 10.244.1.0/24        node-2 owns 10.244.2.0/24
      posts Pod  10.244.1.15            comments Pod  10.244.2.7

    The cluster's Pod range (say 10.244.0.0/16) is split into one /24 per node. The first three numbers tell you the node.

  2. [posts Pod]  eth0 10.244.1.15
         │  veth pair
    [node-1 bridge cni0]

    The packet for 10.244.2.7:4001 leaves the Pod's namespace through its veth pair and arrives on node-1, exactly like the Docker bridge.

  3. node-1 routing table:
      10.244.1.0/24 dev cni0            # my own Pods
      10.244.2.0/24 via 192.168.1.12    # node-2's Pods → send to node-2

    Not one of node-1's own Pods, so the route says: send it to node-2. Some CNIs wrap the packet in a tunnel (VXLAN, an "overlay") instead; same idea.

  4. [node-2] → bridge → veth → [comments Pod] eth0 10.244.2.7 :4001

    node-2 delivers it into the comments Pod. The comments app sees the request coming from 10.244.1.15, the real posts Pod IP. No NAT.

  1. [posts Pod 10.244.1.15] ─veth─┐
                                   ├─ bridge cni0 (node-1)
    [query Pod 10.244.1.22] ─veth─┘

    Both Pods are on node-1. The bridge, a virtual switch, connects them directly. The packet never leaves the machine.

Reality check: you almost never call a Pod IP yourself, because Pod IPs change (chapter 6). But Services, Ingress and everything else end up delivering to Pod IPs, so this flat network is the foundation.

3 · Pod to Service: a virtual IP

A ClusterIP like 10.96.31.40 is not on any machine. No network card has it. It exists only as rules on every node, written by kube-proxy, saying "traffic to 10.96.31.40:4001 → pick one of these Pod IPs". That is NAT plus load balancing.

  1. // in the posts container
    axios.post('http://comments-srv:4001/events', event)

    Your code uses the Service name.

  2. DNS (CoreDNS):  comments-srv  →  10.96.31.40

    The name becomes the Service's virtual IP. See the DNS section below.

  3. packet:  10.244.1.15 → 10.96.31.40:4001
    
    kube-proxy rule on node-1 (iptables / IPVS / nftables):
      10.96.31.40:4001  →  one of:  10.244.2.7:4001
                                     10.244.1.30:4001

    Before the packet leaves node-1, the rule swaps the destination for a real Pod IP, chosen at random. Ready Pods only: the list comes from the Service's EndpointSlice (chapter 11).

  4. packet:  10.244.1.15 → 10.244.2.7:4001   (rewritten)

    From here it is ordinary Pod-to-Pod traffic, as in the previous section.

  5. reply:   10.244.2.7 → 10.244.1.15
    node rewrites source back to 10.96.31.40

    The node remembers the connection (conntrack), so the reply appears to come from the Service IP. posts never knows which Pod answered.

Why ping a ClusterIP fails

The rules only cover the Service's TCP/UDP ports. Ping (ICMP) matches nothing, and nobody owns the address. Test a Service with curl or wget, never ping.

Long connections stick

The Pod is chosen per connection, not per request. A client holding one keep-alive or gRPC connection talks to the same Pod all the time.

DNS inside the cluster

The cluster runs its own DNS server, CoreDNS, itself a Deployment with a Service. Every Pod's /etc/resolv.conf points to it and lists search domains: suffixes tried automatically after a short name. That is why plain posts-srv works. Try names and namespaces:

You writeWorks from
posts-srvPods in the same namespace
posts-srv.defaultAny namespace
posts-srv.default.svc.cluster.localAnywhere in the cluster (the full name)
$ kubectl run tmp --rm -it --image=busybox:1.36 -- nslookup posts-srv
Name:      posts-srv.default.svc.cluster.local
Address:   10.96.20.14

4 · Outside to inside

Pod and Service IPs are private to the cluster. To let a browser in, use one of these (chapter 11 for the first two):

One Ingress routing paths to ServicesUsers reach one load balancer. It sends traffic to an ingress controller, which routes /posts to posts-srv, /comments to comments-srv, and / to client-srv. Users 1 LoadBalancerblog.example.com Ingress controllernginx, reads the path(a reverse proxy) /posts/comments/ posts-srv comments-srv client-srv
Way inHowUse for
kubectl port-forwardA tunnel from your laptopDebugging only
NodePortSame port opened on every nodeLocal clusters, demos
LoadBalancerA cloud load balancer per ServiceOne or two public services
IngressOne reverse proxy routing by host and pathMost web apps: one address, many Services, HTTPS in one place

For the blog app from chapter 17, one Ingress replaces four LoadBalancers:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog
spec:
  ingressClassName: nginx
  rules:
    - host: blog.example.com
      http:
        paths:
          - path: /posts
            pathType: Prefix
            backend: { service: { name: posts-srv, port: { number: 4000 } } }
          - path: /comments
            pathType: Prefix
            backend: { service: { name: comments-srv, port: { number: 4001 } } }
          - path: /
            pathType: Prefix
            backend: { service: { name: client-srv, port: { number: 3000 } } }

An Ingress is only a set of rules. Something must run them: an ingress controller (e.g. Traefik), installed once per cluster. The newer Gateway API does the same job with more features. Chapter 27 covers both in depth.

NetworkPolicy: who may talk to whom

Rule 2 above means: by default every Pod can reach every Pod, in every namespace. A NetworkPolicy is a firewall rule for Pods, chosen by labels. Example: only the event bus may call moderation:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: moderation-only-from-bus
spec:
  podSelector:
    matchLabels: { app: moderation }      # protect these Pods
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: event-bus }   # only from these
      ports:
        - port: 4003

Debugging checklist

"posts cannot reach comments-srv". Go down the list; each step uses an idea from chapter 24:

  1. 1 · Does the name resolve? (DNS)
    $ kubectl exec deployment/posts-depl -- nslookup comments-srv

    Fails → typo, wrong namespace, or the Service does not exist (kubectl get svc -A).

  2. 2 · Does the Service have endpoints? (labels)
    $ kubectl get endpointslices -l kubernetes.io/service-name=comments-srv

    Empty → selector does not match Pod labels, or no Pod is ready.

  3. 3 · Does the Pod answer directly? (IP + port)
    $ kubectl get pods -o wide            # find the comments Pod IP
    $ kubectl exec deployment/posts-depl -- wget -qO- http://10.244.2.7:4001/

    Refused → app not listening on that port, or listening on 127.0.0.1. Check targetPort = the app's port.

  4. 4 · Does the Service answer? (kube-proxy)
    $ kubectl exec deployment/posts-depl -- wget -qO- http://comments-srv:4001/

    Pod works but Service does not → check the Service port/targetPort.

  5. 5 · Timeouts? (policy)
    $ kubectl get networkpolicy -A

    A policy silently dropping traffic is the usual cause of timeouts inside a cluster.

Recap

Basic idea (ch. 24)Where it shows up in Kubernetes
Network namespaceOne per Pod; containers share it → localhost
IP rangesPod range split per node; separate Service range
Routing / bridge / vethCNI plugin connects Pods across nodes
NAT + load balancingkube-proxy rules turn a ClusterIP into a Pod IP
DNSCoreDNS + search domains: posts-srv, posts-srv.team-a
Reverse proxyIngress: one entry point, routes by host and path
FirewallNetworkPolicy
0.0.0.0Apps in Pods must listen on all addresses

Quick check

A Pod in namespace team-a calls http://posts-srv:4000 and gets ENOTFOUND. posts-srv runs fine in default. Fix?
Show answer

Short names only search the caller's own namespace. Use http://posts-srv.default:4000.

ping 10.96.31.40 fails, but curl 10.96.31.40:4001 works. Is the Service broken?
Show answer

No. A ClusterIP is virtual; only its Service ports are forwarded. Ping is not.

Kubernetes networking: common questions

Why can I not ping a Kubernetes ClusterIP?

A ClusterIP is virtual. kube-proxy only forwards the Service ports, so ICMP ping matches nothing. Test with curl or wget instead.

How do I call a Service in another namespace?

Add the namespace to the name, for example http://posts-srv.default:4000. Short names only resolve inside the caller’s own namespace.