← Articulet Kubernetes Zero to Hero Chapter 4.1
Module 4 Networking and Services

Kubernetes Networking Fundamentals

You've mastered Pods, Deployments, and scheduling. Now comes the topic that stumps more Kubernetes learners than anything else: networking. Every Pod needs to talk to other Pods, but how does that work when containers a...

Chapter 8 of 22
Module 4: Networking and Services

Kubernetes networking is the #1 topic that confuses beginners. This module makes it crystal clear — from pod-to-pod communication to external traffic ingress.

Module 4 of 8 | Difficulty: Intermediate

You've mastered Pods, Deployments, and scheduling. Now comes the topic that stumps more Kubernetes learners than anything else: networking. Every Pod needs to talk to other Pods, but how does that work when containers are isolated, Nodes are separate machines, and IPs keep changing? In this chapter, we peel back the curtain on Kubernetes networking and make it crystal clear.


The Kubernetes Networking Model: A Flat World of Pods

Analogy: The Postal System

Imagine a neighborhood where every apartment (Pod) has its own unique street address (IP address). When you send a letter to a friend, you write their address on the envelope and drop it in the mailbox. The postal service handles the rest — no matter which building your friend lives in, the letter arrives. You never need a doorman to translate addresses. Every apartment is directly reachable from every other using one simple, flat address space.

This is exactly how Kubernetes networking works. Every Pod gets its own unique IP address — the IP-per-Pod model. All Pods can communicate directly with all other Pods, regardless of which Node they're running on. There is no NAT hiding Pods behind Node IPs. The network is flat and software-defined.

Why this design? Simplicity. When every Pod has its own IP, you eliminate port-mapping schemes. Container A on Pod 1 can use port 8080, and Container B on Pod 2 can also use port 8080 — because they have different IPs. No port conflicts across Pods. This mirrors how the internet works, making Kubernetes networking feel familiar to anyone with basic IP networking knowledge.

Containers within the same Pod share the same network namespace, meaning they share localhost. In our analogy, this is like roommates in the same apartment sharing a mailroom — they pass notes internally without involving the postal service.

Visual Description:

graph TB subgraph "Node A [Building A]" POD1[Pod: frontend-1<br/>IP: 10.244.1.5] ---|"shared localhost"| SC[sidecar container] POD2[Pod: frontend-2<br/>IP: 10.244.1.6] end subgraph "Node B [Building B]" POD3[Pod: backend-1<br/>IP: 10.244.2.8] POD4[Pod: backend-2<br/>IP: 10.244.2.9] end NET["Flat Pod Network<br/>10.244.0.0/16<br/>Direct Pod-to-Pod, no NAT"] POD1 -.-> NET POD2 -.-> NET POD3 -.-> NET POD4 -.-> NET style NET fill:#a5d6a7,stroke:#2e7d32,stroke-width:3px style POD1 fill:#fff9c4,stroke:#f9a825 style POD2 fill:#fff9c4,stroke:#f9a825 style POD3 fill:#fff9c4,stroke:#f9a825 style POD4 fill:#fff9c4,stroke:#f9a825

Every Pod has a unique IP in the cluster CIDR (here 10.244.0.0/16). Traffic flows directly between Pods without address translation. Containers inside frontend-1 share localhost — if nginx calls curl localhost:8080, it hits the sidecar on the same Pod. But when frontend-1 (IP 10.244.1.5) sends a packet to backend-1 (IP 10.244.2.8), it travels directly across the flat network — just like a letter going across the neighborhood.

The Kubernetes networking model has three fundamental requirements:

  1. All Pods can communicate with all other Pods without NAT. Pod A sends to Pod B's IP; Pod B sees Pod A's real IP as the source.
  2. All Nodes can communicate with all Pods (and vice versa). This enables kubectl logs, kubectl exec, and system services to reach any Pod.
  3. Each Pod gets its own IP. No port clashes, no sharing IPs between Pods.

⚠️ Common Misconception: "Containers in different Pods on the same Node share localhost." No! Only containers inside the same Pod share localhost. Two Pods on the same Node are fully network-isolated — they must communicate via their Pod IPs, just like two apartments in the same building still use the postal system to exchange mail.

🛑 PAUSE & RECALL — 2 minutes

Without looking back:

  1. Why does Kubernetes give every Pod its own IP instead of using the Node's IP with port mapping?
  2. Two containers are in the same Pod. How do they communicate internally? What IP do they use?
  3. Pod A on Node 1 reaches Pod B on Node 2. Does the packet get NATed at Node 1?

Rate your confidence (0-4).


Container Network Interface (CNI): The Postal Service

If the Kubernetes networking model is the rulebook for how mail should flow, the Container Network Interface (CNI) is the postal service that makes it happen. CNI is a standardized plugin architecture that handles IP allocation, interface creation, and routing between Nodes.

When a Pod is scheduled to a Node, the kubelet calls the configured CNI plugin: "Give this new Pod a network interface and an IP." The CNI plugin creates virtual interfaces, connects them to the cluster network, assigns an IP from the Pod CIDR, and sets up routes. From the Pod's perspective, it simply has an Ethernet interface with an IP.

Popular CNIs include Calico (battle-tested, BGP-based policies), Cilium (eBPF-powered, high performance), Flannel (simple overlay, no policies), and Weave (mesh networking). The key takeaway: CNI plugins are interchangeable components satisfying Kubernetes' networking requirements. You don't need deep CNI internals — you need to understand what they do and that they can be swapped.

Visual Description:

graph LR KUBELET[kubelet] -->|"CNI ADD call"| CNI[CNI Plugin] CNI -->|"Allocate IP"| IPAM[IPAM] CNI -->|"Create veth<br/>+ bridge"| NETDEV[Network Devices] CNI -->|"Configure routes<br/>+ iptables/eBPF"| ROUTING[Routing Table] CNI -->|"Enforce Network<br/>Policies"| POLICY[Policy Controller] style KUBELET fill:#ffcc80 style CNI fill:#a5d6a7,stroke:#2e7d32,stroke-width:2px style IPAM fill:#ce93d8

Cluster DNS: The City's Address Book

Every apartment has an address, but how do you find someone without memorizing IPs? You use the city address book — CoreDNS. Every Service gets a DNS name, and CoreDNS resolves it to the Service's ClusterIP.

The naming convention is predictable:

<service-name>.<namespace>.svc.cluster.local

A Service named backend in namespace production resolves as backend.production.svc.cluster.local. Pods in the same namespace can use just the service name (backend). Pods in other namespaces need the full name — like calling a neighbor by first name versus someone across town by full name.

CoreDNS watches the Kubernetes API Server for Service changes. When a Service is created, CoreDNS immediately knows its ClusterIP and can answer queries. When deleted, the record disappears just as quickly.

Here's how DNS resolution flows inside the cluster:

sequenceDiagram participant Pod as Client Pod participant Resolv as /etc/resolv.conf participant CoreDNS as CoreDNS participant API as Kubernetes API Pod->>Resolv: "nslookup backend" Resolv->>Pod: search default.svc.cluster.local Pod->>CoreDNS: "backend.default.svc.cluster.local?" CoreDNS->>API: "Query Service 'backend'" API->>CoreDNS: ClusterIP: 10.96.5.5 CoreDNS->>Pod: A record: 10.96.5.5

The default dnsPolicy is ClusterFirst — Pods use CoreDNS for lookups, and external queries are forwarded upstream. This is the right choice for nearly all workloads.

⚠️ Common Misconception: "DNS is instant when a Service is created." There's typically a few seconds of propagation delay as CoreDNS picks up changes and caches expire.


Network Policies: Security Gates Between Zones

The flat network is elegant, but you rarely want every Pod talking to every other Pod. Network Policies are the security gates between zones in your neighborhood.

⚠️ Common Misconception: "Network Policies deny all traffic by default." No! Without any NetworkPolicy, all Pods can communicate freely — Kubernetes is wide open. NetworkPolicies are additive: they define what is allowed. Everything not explicitly allowed is denied, but only for Pods selected by the policy.

A NetworkPolicy is a Layer 3/4 firewall — it filters by IP, protocol, and port, but not HTTP paths. Here's a default-deny policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  # No ingress rules = deny all incoming

Now let's allow traffic from frontend Pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

This says: "For Pods labeled app=backend, allow incoming TCP on port 8080 only from Pods labeled app=frontend."

Visual Description:

graph LR subgraph "Namespace: production" FE1[frontend-1] -->|"allowed :8080"| BE[backend-1<br/>app: backend] FE2[frontend-2] -->|"allowed :8080"| BE HACK[attacker] -.-x|"DENIED"| BE end style FE1 fill:#a5d6a7 style FE2 fill:#a5d6a7 style BE fill:#fff9c4,stroke:#f9a825,stroke-width:3px style HACK fill:#ef9a9a

🤔 TRY BEFORE YOU SEE

Write a NetworkPolicy for a database Pod labeled app=postgres in namespace data:

  1. Block ALL ingress by default
  2. Allow TCP port 5432 only from Pods labeled app=api in the same namespace
  3. The database must still make DNS queries (egress to CoreDNS on UDP 53)

Write the YAML before looking at the solution.


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-policy
  namespace: data
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 5432
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53

Notice the egress rule allowing DNS — this is critical! If you enforce egress without allowing DNS, Pods can't resolve Service names and break in confusing ways. The k8s-app: kube-dns label targets CoreDNS Pods.

🛑 PAUSE & RECALL — 3 minutes

Without looking back:

  1. What is the default traffic behavior when NO NetworkPolicy exists?
  2. Your backend Pod can't reach CoreDNS after applying an egress NetworkPolicy. Why?
  3. A NetworkPolicy selects Pods by label app=web. Does this podSelector apply to source, destination, or both?

Rate your confidence (0-4).


GKE in Practice: VPC-Native Networking

GKE Note: GKE provides routes-based (legacy) and VPC-native (modern) networking. Always use VPC-native — it's the default for new clusters and the only option for Autopilot.

VPC-native clusters use alias IP ranges (also called IP aliasing). Rather than hiding Pod IPs behind Node IPs with an overlay, GKE assigns Pod IPs directly from your VPC subnet. This means Pod IPs are first-class VPC citizens — routable from Compute Engine VMs, Cloud SQL, and other GCP resources without tunneling. No route table manipulation is needed; alias IPs scale cleanly.

When creating a VPC-native cluster, you specify three ranges:

Range Purpose Example
Node CIDR IPs for Nodes 10.0.0.0/24
Pod CIDR (alias) IPs for Pods 10.4.0.0/14
Service CIDR (alias) ClusterIPs for Services 10.8.0.0/20

These must not overlap with each other or peered VPCs. Plan carefully — Pod and Service CIDRs cannot be changed after cluster creation.

GKE Dataplane V2, built on Cilium eBPF, replaces kube-proxy with a unified kernel-space dataplane providing built-in Network Policy enforcement, network visibility with flow logs, and improved performance. Enable it at cluster creation:

gcloud container clusters create my-cluster \
  --enable-dataplane-v2 \
  --enable-network-policy

GKE Note: On GKE Autopilot, Dataplane V2 is enabled by default and cannot be disabled.


Lab: LAB-4.1 — Networking Fundamentals (60 min)

Step 1: Deploy Test Pods

kubectl create namespace lab-networking

kubectl run frontend --namespace lab-networking \
  --image=nginx:alpine --labels="app=frontend,tier=web"

kubectl run backend --namespace lab-networking \
  --image=radial/busyboxplus:curl --labels="app=backend,tier=api" \
  -- sleep 3600

kubectl wait --for=condition=Ready pod/frontend pod/backend \
  --namespace lab-networking --timeout=60s

Step 2: Verify Pod-to-Pod Communication

# Get Pod IPs
kubectl get pods -n lab-networking -o wide

# Test from backend to frontend using Pod IP (replace with actual)
kubectl exec backend -n lab-networking -- curl -s -o /dev/null \
  -w "HTTP %{http_code}" http://10.244.0.15
# Expected: HTTP 200

You used the Pod's direct IP — no Service involved. This proves the flat network works.

Step 3: Test DNS Resolution

kubectl expose pod backend -n lab-networking --port=80 --target-port=8080

kubectl exec frontend -n lab-networking -- nslookup backend
# Expected: resolves to backend.lab-networking.svc.cluster.local

Step 4: Apply Default-Deny and Observe

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: lab-networking
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
EOF

Now test:

# This will TIME OUT
kubectl exec backend -n lab-networking -- \
  curl -s --connect-timeout 5 http://frontend \
  || echo "Connection blocked as expected!"

The connection is blocked! This is the "aha" moment — default-deny is powerful but dangerous.

Step 5: Restore Selective Access

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: lab-networking
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 80
EOF

Step 6: Inspect CNI (on GKE Standard or accessible nodes)

# View CNI configuration on a node
gcloud compute ssh $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') \
  --command "cat /etc/cni/net.d/*.conf"

On GKE with Dataplane V2, you'll see Cilium configurations. On Calico clusters, you'll see Calico CNI configs.

Cleanup

kubectl delete namespace lab-networking

Chapter Summary

Kubernetes networking rests on a simple foundation: every Pod gets its own IP, all Pods communicate directly without NAT, and containers within a Pod share localhost. CNI plugins handle the behind-the-scenes work of IP allocation and routing. CoreDNS provides service discovery through a predictable DNS naming scheme. Network Policies add Layer 3/4 security, but remember — the default is wide open, and policies define what is allowed. On GKE, VPC-native networking gives Pods first-class VPC IPs via alias ranges, while Dataplane V2 delivers eBPF-based performance and built-in policy enforcement.


📇 KEY CONCEPT CARDS

  1. Q: What are the three fundamental requirements of the Kubernetes networking model?
    A: (1) All Pods can communicate with all other Pods without NAT. (2) All Nodes can communicate with all Pods (and vice versa). (3) Every Pod gets its own unique IP address.
  1. Q: What does a CNI plugin do, and when is it invoked?
    A: A CNI plugin configures network interfaces, allocates IPs, and sets up routing for Pods. It is invoked by the kubelet whenever a Pod is created or destroyed on a Node.
  1. Q: What is the full DNS name for a Service named api in namespace payments?
    A: api.payments.svc.cluster.local. Pods in the same namespace can use the short name api.
  1. Q: What is the default traffic behavior when NO NetworkPolicy exists? What happens with a policy that has empty ingress rules?
    A: With no policy, all traffic is allowed. A policy with empty ingress rules denies all incoming traffic for the selected Pods. Default-deny requires explicitly applying a policy that selects all Pods with no rules.