Anurag's Notes

Networking

← 08 Storage · Index · 10 Design and Install a Kubernetes Cluster →

Contents


9.1 Networking Introduction

Source: KodeKloud notes

Prerequisite checklist (Linux networking basics)

TopicWhy it mattersCommands / files
Interface & IP configNodes and Pods need correct addressingip link, ip addr
Gateways & routesTraffic to other subnets / internetip route, route -n
DNS & name resolutionK8s service discovery is DNS-based/etc/resolv.conf, dig, nslookup
Network namespacesContainers isolate network stacks; CNI plugins manipulate namespacesip netns, docker run --net=...
  • Prereq lessons (§9.2–§9.6) are optional/brief; network namespaces + Docker networking make the K8s material much easier.

Section roadmap (each builds on the previous)

LessonKey takeaways
Cluster networking requirements (§9.7)Node IP addressing, routing, ports/firewall constraints
Pod networking (§9.9)Pod IPs, Pod-to-Pod communication, namespaces; localhost vs Pod network
CNI in Kubernetes (§9.10–§9.12)Plugin types; how CNI configures interfaces, routes, IPs
Cluster DNS (§9.15–§9.16)CoreDNS; DNS records for Services and Pods
Service networking, LB, Ingress, Gateway API (§9.13, §9.18, §9.21)ClusterIP, NodePort, external LoadBalancer, Ingress, Gateway API (north-south traffic)

9.2 Prerequisite Switching Routing Gateways CNI in kubernetes

Source: KodeKloud notes

Prereq topics: switching, routing, default gateway · DNS on Linux, CoreDNS intro · network namespaces · Docker networking.

Switching (same subnet)

  • Network = hosts exchanging packets. Hosts A and B on the same switch + each has an interface with an IP in the same subnet → talk directly.
  • Switch forwards frames within one L2 broadcast domain (≈ one IP subnet). It cannot carry traffic to another subnet.
ip link                                  # list interfaces (e.g. lo, eth0) and state
sudo ip addr add 192.168.1.10/24 dev eth0   # on host A
sudo ip addr add 192.168.1.11/24 dev eth0   # on host B
ping 192.168.1.11                        # from A → works via the switch

Routing and gateways (different subnets)

  • Router = device with an interface (IP) in each subnet (e.g. 192.168.1.1 and 192.168.2.1), forwards between them.
  • Gateway = a route entry telling a host "to reach network X, send to this router IP". Without a route: ping: connect: Network is unreachable.

Two subnets 192.168.1.0 (A, B) and 192.168.2.0 (C, D) joined by a router with 192.168.1.1 and 192.168.2.1

route -n                                         # kernel routing table (same as: ip route)
sudo ip route add 192.168.2.0/24 via 192.168.1.1 # on A/B: reach 2.0 net via router's 1.x interface
sudo ip route add default via 192.168.2.1        # on a 192.168.2.0/24 host: default route → internet router
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.2.1     0.0.0.0         UG    0      0        0 eth1   <- default / 0.0.0.0 route
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 eth0   <- Gateway 0.0.0.0 = directly connected, no router
192.168.2.0     0.0.0.0         255.255.255.0   U     0      0        0 eth1
  • Flags: U = up, G = uses a gateway.
  • Multiple routers (one internal, one internet): specific routes for internal nets + default for the rest:
    sudo ip route add 192.168.1.0/24 via 192.168.2.2   # internal router
    sudo ip route add default via 192.168.2.1          # internet router
  • Internet connectivity problems → first check routing table and that a valid default gateway exists.

Linux host as a router

Setup: A 192.168.1.5 — B (eth0 192.168.1.6, eth1 192.168.2.6) — C 192.168.2.5.

# on A: reach C's network via B
sudo ip route add 192.168.2.0/24 via 192.168.1.6
# on C: reach A's network via B (return path!)
sudo ip route add 192.168.1.0/24 via 192.168.2.6

# on B: Linux does NOT forward packets between interfaces by default
cat /proc/sys/net/ipv4/ip_forward                    # 0 = disabled, 1 = enabled
sudo sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'  # enable (temporary), or:
sudo sysctl -w net.ipv4.ip_forward=1                 # enable (temporary)
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf   # persist
sudo sysctl -p                                       # reload /etc/sysctl.conf

Persistence

  • ip addr / ip route changes are lost on reboot. Persist via distro network config: Debian/Ubuntu /etc/network/interfaces or Netplan; RHEL/CentOS NetworkManager or /etc/sysconfig/network-scripts. IP forwarding → /etc/sysctl.conf.

Quick reference

CommandPurpose
ip linkList/modify interfaces
ip addrShow IPs on interfaces
ip addr add <addr>/<prefix> dev <iface>Assign IP
ip route / route -nShow routing table
ip route add <net>/<prefix> via <gw>Add route
ip route add default via <gw>Add default route (0.0.0.0/0)
cat /proc/sys/net/ipv4/ip_forwardCheck forwarding (0/1)
sysctl -w net.ipv4.ip_forward=1Enable forwarding (temporary)
/etc/sysctl.conf + sysctl -pPersist forwarding

DNS / CoreDNS and namespaces (preview)

  • Linux resolver config: /etc/resolv.conf (may be managed by systemd-resolved / NetworkManager — check the effective config). Details §9.3.
  • CoreDNS = default cluster DNS in K8s (Pods resolve my-service.default.svc.cluster.local). DNS troubleshooting:
    kubectl get pods -n kube-system                   # CoreDNS pods running?
    kubectl logs -n kube-system <coredns-pod>         # CoreDNS errors
    kubectl exec <pod> -- cat /etc/resolv.conf        # Pod's resolver config / dnsPolicy correct?
  • Network namespace = own interfaces, routing table, firewall rules; containers use them for isolation. ip netns (list), ip netns exec <ns> <cmd>, then ip link/ip addr/ip route inside. Details §9.4.

[!WARNING] Exam traps / remember:

  • Routes are needed in both directions (A→C and C→A) when using a host as router.
  • A Linux box with two NICs won't route until net.ipv4.ip_forward=1.
  • ip-command changes are not persistent; sysctl persistence = /etc/sysctl.conf + sysctl -p.

[!NOTE] Beyond the course: kubeadm's install prerequisites require net.ipv4.ip_forward = 1 on every node (usually put in /etc/sysctl.d/k8s.conf, applied with sudo sysctl --system); preflight fails without it. Relevant if asked to build/repair a node (§11).

9.3 Prerequisite DNS

Source: KodeKloud notes

Scenario: host A 192.168.1.10, host B 192.168.1.11 (database). Goal: reach B as db.

Local resolution: /etc/hosts

ping db                                   # ping: unknown host db
echo '192.168.1.11    db' | sudo tee -a /etc/hosts
ping db                                   # PING db (192.168.1.11) ...
  • Translating a name → IP (here via /etc/hosts) is name resolution.
  • /etc/hosts is local-only and trusted as-is: A uses the entry even if B's real hostname is host-2. You can even map www.google.com to 192.168.1.11 locally.
  • Many entries possible (web, db, nfs); used by ping, ssh, curl, etc.
  • Doesn't scale: every IP change must be edited on every host.

Central DNS server: /etc/resolv.conf

cat /etc/resolv.conf
# /etc/resolv.conf
nameserver 192.168.1.100      # DNS server; several nameserver lines allowed
search mycompany.com prod.mycompany.com   # appended to short names
  • Name not in /etc/hosts → query the nameserver(s). IP change → update DNS server only.
  • /etc/hosts still usable alongside DNS for local/test names (e.g. 192.168.1.115 test).
  • Search domains: ping web tries web.mycompany.com, then web.prod.mycompany.com.
  • FQDN with trailing dot (web.mycompany.com.) = absolute → no search domains appended. ndots option also controls when search domains are tried.
  • Direct edits to /etc/resolv.conf may be overwritten by DHCP / NetworkManager / systemd-resolved → persist through those tools.

Lookup order: /etc/nsswitch.conf

hosts:      files dns
  • files = /etc/hosts, dns = servers in /etc/resolv.conf. Default: hosts file first; DNS only if not found. Reorder to let DNS win.

DNS hierarchy and recursive resolution

  • Root . → TLD (.com, .net, .org, …) → domain (google) → subdomains (www, mail, drive, maps, apps).

DNS tree: root ., TLD .com, domain google, subdomains mail/drive/www/maps/apps

  • Org DNS not authoritative for a name → asks root → .com servers → google.com authoritative servers → IP; Org DNS caches answer for the TTL.

Resolving apps.google.com: Org DNS → Root DNS → .com DNS → Google DNS → 216.58.221.78, cached

Record types

TypeMapsExample
Aname → IPv4web-server → 192.168.1.1
AAAAname → IPv6web-server → 2001:0db8:85a3::8a2e:0370:7334
CNAMEname → another name (alias)eat.web-server → food.web-server, hungry.web-server → food.web-server (one canonical name per alias)
  • CNAME result must then be resolved to an A/AAAA record.

Testing tools

ToolUses /etc/hosts?Use
pingYes (full resolver: hosts → nsswitch order → DNS)Basic reachability by name
nslookup <name>No — queries DNS server directlyWhat does DNS return?
dig <name>NoDetailed answer: records, TTL, server, query time
nslookup www.google.com      # Server: 8.8.8.8 / Non-authoritative answer: ... Address: 172.217.0.132
dig www.google.com           # ANSWER SECTION: www.google.com. 245 IN A 64.233.177.103 ...; SERVER: 8.8.8.8#53

[!WARNING] Exam traps / remember:

  • Name resolves via ping but not via nslookup/dig → it exists only in /etc/hosts, not in DNS.
  • Lookup order is set by /etc/nsswitch.conf (hosts: files dns), not by resolv.conf.
  • DNS = port 53. Pods' /etc/resolv.conf (nameserver = CoreDNS Service IP, cluster search domains) → §9.15/§9.16.

9.4 Prerequisite Network Namespaces

Source: KodeKloud notes

Concept

  • Host = house, namespaces = rooms: each child (container) sees only its room; parent (host) sees everything.
  • Container runtime creates a namespace per container → container sees only its own processes (e.g. nginx as PID 1) and network devices; the host sees the same process with a different PID (e.g. 3816) among all others.
  • A network namespace has its own interfaces, routing table and ARP table; the host keeps its own (e.g. eth0 192.168.1.2 on LAN 192.168.1.0).
  • All commands below need root (sudo).

Create / inspect

ip netns add red; ip netns add blue   # create
ip netns list                         # or: ip netns
ip link                               # host: lo, eth0
ip netns exec red ip link             # inside red: only lo
ip -n red link                        # shorthand for the same
ip netns exec red arp                 # ARP table inside ns (also: arp -n)
ip netns exec red route               # routing table inside ns (also: ip -n red route)

Connect two namespaces directly: veth pair ("virtual cable")

ip link add veth-red type veth peer name veth-blue   # create pair
ip link set veth-red netns red                        # move each end into a ns
ip link set veth-blue netns blue
ip -n red  addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
ip -n red  link set veth-red up
ip -n blue link set veth-blue up
ip netns exec red ping -c 1 192.168.15.2              # works
ip netns exec blue arp -n                             # shows 192.168.15.1 on veth-blue
arp -n                                                # host ARP knows nothing of these

Many namespaces: Linux bridge (virtual switch)

Diagram (new): red/blue namespaces on bridge v-net-0, host routing, ip_forward, MASQUERADE and DNAT to the LAN Diagram (new): namespaces joined via veth pairs to host bridge v-net-0; host forwards + NATs to eth0/LAN.

ip link add v-net-0 type bridge        # bridge = just another interface on the host
ip link set dev v-net-0 up
ip -n red link del veth-red            # remove the old direct pair (deletes BOTH ends)

ip link add veth-red  type veth peer name veth-red-br
ip link add veth-blue type veth peer name veth-blue-br
ip link set veth-red  netns red
ip link set veth-blue netns blue
ip link set veth-red-br  master v-net-0     # attach host-side end to bridge
ip link set veth-blue-br master v-net-0
ip link set veth-red-br up; ip link set veth-blue-br up

ip -n red  addr add 192.168.15.1/24 dev veth-red
ip -n blue addr add 192.168.15.2/24 dev veth-blue
ip -n red  link set veth-red up
ip -n blue link set veth-blue up

[!CAUTION] Correction: the notes delete the old pair with ip link delete veth-red. After veth-red was moved into red it no longer exists in the host namespace, so that fails ("Cannot find device"). Use ip -n red link del veth-red (the lecture's command). Deleting either end removes both.

Host ↔ namespaces

ip addr add 192.168.15.5/24 dev v-net-0   # give bridge an IP → host joins 192.168.15.0/24
ping -c 1 192.168.15.1                    # now works from host
  • Network is private to the host: nothing outside can reach 192.168.15.0/24, namespaces can't reach outside.

Namespaces → outside (routing + forwarding + NAT)

Host eth0 192.168.1.2, bridge 192.168.15.5, blue 192.168.15.2.

ip netns exec blue ip route add 192.168.1.0/24 via 192.168.15.5   # LAN via host
ip netns exec blue ip route add default via 192.168.15.5          # or: everything via host
sysctl -w net.ipv4.ip_forward=1                                   # host must forward (persist in /etc/sysctl.conf)
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE   # source-NAT to host IP so replies return
# optionally restrict: ... -s 192.168.15.0/24 -o eth0 -j MASQUERADE
ip netns exec blue ping -c 1 8.8.8.8
  • Without NAT: packets reach the LAN host but it has no route back to 192.168.15.0/24 → no reply.

Outside → service in a namespace

  1. Add routes on remote hosts pointing to 192.168.15.0/24 via 192.168.1.2 (impractical), or
  2. Port forward (DNAT) on the host:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.15.2:80
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
  • Filter table FORWARD chain must also accept the forwarded packets.

Quick reference

TaskCommand
Create / list nsip netns add red · ip netns list
Run in nsip netns exec red ip link · ip -n red link
veth pairip link add veth-red type veth peer name veth-blue
Move into nsip link set veth-red netns red
Bridgeip link add v-net-0 type bridge
Attach to bridgeip link set veth-red-br master v-net-0
Bring upip link set v-net-0 up · ip -n red link set veth-red up
Route in nsip -n blue route add default via 192.168.15.5
Forwardingsysctl -w net.ipv4.ip_forward=1
SNATiptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
DNATiptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.15.2:80

[!WARNING] Exam traps / remember:

  • Interfaces must be brought up on both sides (ns end and bridge end) and the bridge itself.
  • A route of 192.168.1.0/24 via … only covers the LAN; reaching 8.8.8.8 needs the default route.
  • Egress = POSTROUTING MASQUERADE; ingress = PREROUTING DNAT. iptables rules are not persistent across reboots unless saved.
  • This 8-step pattern (ns → bridge → veth pair → attach to ns → attach to bridge → IP → up → NAT) is exactly what CNI plugins automate (§9.6).
ps aux   # inside container: nginx is PID 1 (only its own processes)
ps aux   # on host: same nginx appears as e.g. PID 3816 among all host processes

9.5 Prerequisite Docker Networking

Source: KodeKloud notes

Assumed: single Docker host, eth0 192.168.1.10.

Network modes

ModeBehaviourExample
noneOnly loopback; no connectivity to other containers or outside (add interfaces manually if needed)docker run --network none nginx
hostShares host's network namespace, no isolation; app on :80 is reachable on host :80; two containers can't bind the same portdocker run --network host nginx
bridge (default)Own netns per container, attached to host bridge docker0 via vethdocker run nginx

Bridge internals

  • Docker network named bridge = host interface docker0. Default subnet historically 172.17.0.0/16 (examples show 172.17.0.1/24 on docker0).
  • docker0 is DOWN (NO-CARRIER) until a container attaches.
  • Per container: Docker creates a netns + veth pair; one end attached to docker0 (vethXXXX), other end inside container as eth0, IP e.g. 172.17.0.3.
  • Same steps as manual §9.4 setup, just Docker's naming.
docker network ls                 # bridge (driver bridge), host (host), none (null)
ip link                           # docker0 on host; vethbb1c343@if7 ... master docker0
ip addr show docker0              # inet 172.17.0.1/24
ip netns                          # b3165c10a92b (container netns, if visible)
ip -n b3165c10a92b link           # 7: eth0@if8
ip -n b3165c10a92b addr           # inet 172.17.0.3/24
docker inspect <container>        # NetworkSettings.SandboxKey: /var/run/docker/netns/b3165c10a92b
  • Pairing veth ends: kernel assigns consecutive interface indices; eth0@if8 (index 7, in container) ↔ vethbb1c343@if7 (index 8, on host). @ifN = peer's index.

docker0 bridge 172.17.0.1 with three containers; each eth0@ifN in the container pairs with vethXXX@ifM on the bridge

  • If ip netns doesn't list Docker's namespaces (Docker doesn't register them in /var/run/netns), use nsenter or docker exec.

Reachability and port publishing

  • Containers on the same bridge talk via private IPs; host reaches them too: curl http://172.17.0.3:80.
  • Outside the host → not reachable until a port is published:
docker run -p 8080:80 nginx       # host :8080 → container :80; clients use http://192.168.1.10:8080
sudo iptables -nvL -t nat         # Docker's DNAT rule in chain DOCKER:
# DNAT  tcp  --  0.0.0.0/0  0.0.0.0/0  tcp dpt:8080 to:172.17.0.2:80
  • Docker implements -p with iptables NAT (DNAT) rules — same technique as §9.4.

[!CAUTION] Correction: the notes show iptables -nvl -t nat; the list option is uppercase -L → iptables -nvL -t nat.

[!NOTE] Beyond the course: dockershim was removed in v1.24; Kubernetes nodes run containerd/CRI-O, use crictl there, and the pod bridge is created by the CNI plugin (commonly cni0, not docker0) — see §9.8.

  • -p = --publish (docker run --publish 8080:80 nginx).

9.6 Prerequisite CNI

Source: KodeKloud notes

Why CNI

  • Docker, rkt, Mesos, Kubernetes all repeat the same steps: create netns → create bridge → create veth pair → attach one end to ns → other end to bridge → assign IPs → bring up → enable NAT/IP masquerade.

Same 8 networking steps repeated by Docker, rkt, Mesos and Kubernetes

  • Idea: put those steps in one program (e.g. bridge) the runtime calls with the container's netns: bridge add 2e34dcf34 /var/run/netns/2e34dcf34 (illustrative).
  • CNI = the standard defining how plugins are written and how runtimes invoke them → any CNI runtime works with any CNI plugin.

Diagram (new): runtime invokes CNI plugin with env vars + stdin config, plugin configures netns, IPAM delegated, result on stdout Diagram (new): CNI invocation contract between container runtime, plugin and IPAM plugin.

Responsibilities

Container runtime mustCNI plugin must
Create a network namespace per containerSupport ADD, DEL, CHECK commands
Identify which network(s) the container joinsAccept params: container ID, netns path, (interface name)
Invoke plugin on container create (ADD) and delete (DEL)Configure network: veth, bridge attach, IP assignment, routes
Pass config in JSON; set env CNI_COMMAND, CNI_CONTAINERID, CNI_NETNS, CNI_IFNAME; config JSON on stdinReturn result (IPs, routes, interfaces) in the defined JSON format on stdout
  • Host-side network config: JSON files in /etc/cni/net.d/. IP address management (IPAM) is a separate plugin.

Plugins

TypePlugins / use
Built-in networkingbridge (Linux bridge, same L2), vlan (VLAN-segmented), ipvlan (lightweight L2/L3 split, high perf), macvlan (unique MAC on host NIC, directly on L2), Windows (win-bridge, win-overlay)
IPAMhost-local (local allocation), dhcp
Third-party (implement CNI)Weave, Flannel, Cilium, VMware NSX, Calico

CNI: rkt/Mesos/Kubernetes; plugins BRIDGE, VLAN, IPVLAN, MACVLAN, WINDOWS; IPAM host-local, DHCP; Flannel, Cilium, NSX, Calico

{
  "cniVersion": "0.4.0",
  "name": "mynet",
  "type": "bridge",
  "bridge": "cni0",
  "isDefaultGateway": true,
  "ipMasq": true,
  "ipam": { "type": "host-local", "subnet": "10.22.0.0/16" }
}
  • type = plugin binary to run; bridge = bridge name; ipMasq = NAT; ipam.type = IPAM plugin + its subnet.

Docker is the exception

  • Docker uses its own standard CNM (Container Network Model), not CNI → can't do docker run --network=<cni-plugin>.
  • Workaround (what Kubernetes did with Docker): create the container with --network=none, then invoke the CNI plugin against its netns.
docker run --network=none --name mynginx -d nginx
PID=$(docker inspect --format '{{.State.Pid}}' mynginx)   # container's PID
NETNS="/proc/${PID}/ns/net"                               # its network namespace path
bridge add mynginx "${NETNS}"                             # illustrative plugin call

[!WARNING] Exam traps / remember:

  • CNI plugin commands: ADD, DEL, CHECK. Runtime creates the netns; the plugin wires it up.
  • Config dir /etc/cni/net.d/; IPAM plugins = host-local, dhcp.
  • Docker = CNM, not CNI.

[!NOTE] Beyond the course: plugin binaries live in /opt/cni/bin/ (§9.10). With dockershim gone (v1.24), the CRI runtime (containerd/CRI-O) invokes CNI directly — no --network=none trick. Current CNI spec versions are 1.0.x/1.1.x (also a VERSION command; 1.1 adds GC/STATUS). Weave Net is no longer maintained (Weaveworks shut down 2024); labs may still use it, but prefer Calico/Flannel/Cilium manifests when installing a CNI.

9.7 Cluster Networking

Source: KodeKloud notes

Node requirements

  • ≥1 working interface (e.g. eth0) per node (master and worker), with an IP reachable by the nodes that must talk to it.
  • Unique hostname per node.
  • Unique MAC address per node — watch out for VMs cloned from templates/snapshots: regenerate MAC + set hostname before joining.

master-01 192.168.1.10 and worker-01/02 .11/.12, each with its own hostname and MAC, on network 192.168.1.0

Required ports

ComponentPort (TCP)Who connects / notes
kube-apiserver6443Everything: other control-plane components, kubelets (all nodes), kubectl / external clients
kubelet (every node, incl. control plane)10250Control plane → kubelet API (exec, attach, port-forward, logs)
kube-scheduler10259Secure port (self)
kube-controller-manager10257Secure port (self)
etcd client2379kube-apiserver → etcd
etcd peer2380Between etcd members (multi-master)
NodePort Services30000–32767Worker nodes; external clients

kubernetes.io ports-and-protocols table: control plane 6443, 2379-2380, 10250, 10259, 10257; workers 10250, 30000-32767

  • etcd ports are listed under control plane, but etcd may be external or on custom ports.
  • Reference: kubernetes.io → Ports and Protocols (allowed in exam docs).
  • Open on: host firewalls (iptables, nftables, firewalld, ufw), cloud rules (AWS Security Groups, GCP firewall rules, Azure NSGs), network ACLs.

Inspection commands

kubectl get nodes -o wide                     # INTERNAL-IP per node
ip addr show                                  # all interfaces + IPs
ip link show eth0                             # link details incl. MAC (link/ether)
cat /sys/class/net/eth0/address               # MAC only
hostnamectl status                            # or: hostname
sudo ss -ltnp                                 # listening TCP sockets + owning process
sudo ss -ltnp | egrep '6443|10250|10259|10257|2379|2380'
sudo nft list ruleset                         # nftables
sudo iptables -L -n -v                        # legacy iptables
sudo ufw status                               # ufw
sudo firewall-cmd --list-all                  # firewalld
  • Test TCP reachability: ss, nc/telnet, curl (HTTP endpoints), e.g. from a worker:
    nc -zv <control-plane-ip> 6443                 # API server reachable from this node?
    curl -k https://<control-plane-ip>:6443/livez  # HTTP-level check; extra endpoint explained below

Troubleshooting checklist

  • Node IPs/hostnames unique and resolvable (DNS or /etc/hosts); cloned VMs have unique MACs.
  • Workers → control plane 6443; control plane ↔ control plane 2380 (+ 2379 as needed); control plane → kubelets 10250.
  • Check cloud security groups as well as host firewalls.
  • Then move on to CNI, Service-IP routing, overlay diagnostics (§12.8).

[!WARNING] Exam traps / remember:

  • Blocking 2380 between etcd members breaks quorum → control plane down.
  • kubelet 10250 is on all nodes, not just workers.
  • Scheduler 10259, controller-manager 10257 (secure). Old insecure ports 10251/10252 are gone.
  • NodePort default range 30000–32767.

[!NOTE] Beyond the course: kubeadm also requires a unique product_uuid per node (sudo cat /sys/class/dmi/id/product_uuid). Other common ports: kube-proxy health 10256 (all nodes). netstat -plnt is an alternative to ss -ltnp where installed.

[!NOTE] Beyond the course: /livez and /readyz on the API server’s secure port are health checks (anonymous access depends on cluster authentication/RBAC policy). Scheduler/controller-manager insecure ports 10251/10252 are historical; current secure ports are 10259/10257.

9.8 Lab Solution Explore Environment optional

Source: KodeKloud notes

Drill (example values from the lab; yours will differ):

#TaskCommandHow to read / example
1Node countkubectl get nodesCount rows minus header → 2 (controlplane, node01)
2Control-plane internal IPkubectl get nodes -o wideINTERNAL-IP column → 192.5.114.3
3Interface used for cluster connectivity (control plane)ip address show (on controlplane)Interface holding the INTERNAL-IP → eth0
4MAC of that interfaceip address show eth0link/ether value → 52:54:00:72:06:aa
5node01 IPkubectl get nodes -o wide192.5.114.6
6node01 MACssh node01 → ip address show eth0Copy link/ether exactly
7Bridge created by containerd/CNIip -d link show type bridgecni0
8State of cni0ip link show cni0Read the state field (lab: UP)
9Default gateway (route to Google)ip routedefault via 172.25.0.1 dev eth1 → 172.25.0.1
10kube-scheduler listening portsudo ss -ltnp | grep kube-scheduler127.0.0.1:10259 → 10259
ip a | grep -B2 192.5.114.3          # speed: find the interface that owns the node IP
ip route | grep default              # gateway only
  • INTERNAL-IP = node/host IP; Pod/CNI addresses (e.g. 10.244.0.1/16 on cni0) are a separate network.
  • Default route may leave via a different interface (eth1) than the cluster interface (eth0).

[!WARNING] Exam traps / remember:

  • Interface state ≠ the UP inside <...> flags: <...,UP> = administratively up; state UP/DOWN = operational. NO-CARRIER usually means no veth attached yet. Answer with the state field.
  • Node MAC questions: SSH to that node — kubectl doesn't show MACs.
  • 10251 = old insecure scheduler port (removed); current = 10259.

[!NOTE] Beyond the course: the live lab also asks which etcd port has more client connections — count with sudo netstat -anp | grep etcd | grep 2379 | wc -l vs ... grep 2380 ...; 2379 (client port, used by kube-apiserver) has far more. 2380 is peer-only.

  • kubectl get nodes -o wide also shows OS-IMAGE, KERNEL-VERSION, CONTAINER-RUNTIME (e.g. containerd://1.6.6) → quickest way to identify the node runtime/version without SSH.
default via 172.25.0.1 dev eth1 proto static          # internet gateway (Q9)
172.25.0.0/24 dev eth1 proto kernel scope link src 172.25.0.10
192.5.114.0/24 dev eth0 proto kernel scope link src 192.5.114.3   # node/cluster network
10.244.0.0/16 dev cni0 proto kernel scope link src 10.244.0.1     # pod network via CNI bridge

9.9 Pod Networking

Source: KodeKloud notes

Starting point: control plane and kubelets are up and nodes can reach each other on the underlying network (firewalls / security groups open). Kubernetes does not ship pod networking itself; it delegates it to a pluggable CNI solution.

Kubernetes pod networking model (requirements any solution must meet)

RequirementWhyTypical implementation
Every Pod gets a unique IPPod = first-class network endpointe.g. 10.244.1.2
Pod ↔ every Pod on the same node via that IPLocal traffic routable at L2/L3veth pair + node bridge
Pod ↔ every Pod on other nodes via that IPPod IPs form one cluster-wide routing domainhost routes, central router, overlay
No NAT between PodsApps can use the Pod IP for identity / TLSend-to-end, no SNAT
  • No mandated IP range: any private range is fine, but the CNI must allocate IPs automatically and make them routable cluster-wide.
  • Built from Linux primitives: network namespaces, veth pairs, bridges (or VXLAN / other datapaths), routes (see §9.4).

Manual build on one node (what a CNI plugin automates)

Per node: one bridge with a per-node subnet (node1 10.244.1.0/24, node2 10.244.2.0/24, …); per Pod: veth pair, IP, default route.

# 1) Node datapath: bridge + gateway IP for this node's pod subnet
ip link add v-net-0 type bridge
ip link set dev v-net-0 up
ip addr add 10.244.1.1/24 dev v-net-0

# 2) Per Pod: veth pair ("red" = the Pod's network namespace)
ip link add veth-red type veth peer name veth-red-br

# 3) Pod end into the namespace; IP + default route via the bridge
ip link set veth-red netns red
ip -n red addr add 10.244.1.2/24 dev veth-red
ip -n red link set veth-red up
ip -n red route add default via 10.244.1.1

# 4) Host end onto the bridge
ip link set veth-red-br master v-net-0
ip link set veth-red-br up
  • Inside a namespace always use ip -n <ns> … or ip netns exec <ns> ….
  • After this, Pods on the same node can talk; cross-node needs routing.

Inter-node routing

Per-node pod subnets with route table: 10.244.1/2/3.0/24 via node IPs 192.168.1.11/12/13, aggregated as 10.244.0.0/16

Each host needs a route to every other node's pod subnet via that node's IP:

# node1 (192.168.1.11) -> pods on node2
ip route add 10.244.2.0/24 via 192.168.1.12
# node2 (192.168.1.12) -> pods on node1
ip route add 10.244.1.0/24 via 192.168.1.11
$ ping 10.244.2.2          # before the route
ping: connect: Network is unreachable
$ ping 10.244.2.2          # after the route
64 bytes from 10.244.2.2: icmp_seq=1 ttl=64 time=0.50 ms

Static per-host routes don't scale (N×(N−1) routes). Alternatives:

  • Central router: hold all per-node routes there and use it as default gateway; the whole pod network becomes one 10.244.0.0/16.
  • Overlay (VXLAN, GRE): encapsulate pod traffic between hosts.
  • Routing-based datapath (BGP / MPLS): nodes advertise their pod subnets automatically.
  • CNI plugins automate route distribution or encapsulation for you.

Where CNI fits

  • Per-Pod steps must be automatic and fast (thousands of Pods created/destroyed per minute) and coordinated with the container runtime → CNI is the integration contract.
  • Runtime executes the plugin binary on Pod lifecycle events, passing JSON config on stdin (Pod name, namespace, container ID, network config); plugin does the work and returns a JSON result incl. the allocated IP.
CommandPlugin must
ADD (attach Pod)create veth pair → move one end into Pod netns → attach host end to datapath (bridge / VXLAN / kernel datapath) → allocate + assign IP via IPAM → default route + any iptables rules
DEL (Pod removed)remove interface(s) → free the IP in IPAM
# ADD (template)
ip link add veth0 type veth peer name veth0-br
ip link set veth0 netns <namespace>
ip -n <namespace> addr add <pod-ip>/<prefix> dev veth0
ip -n <namespace> link set veth0 up
ip -n <namespace> route add default via <bridge-ip>
ip link set veth0-br master v-net-0
ip link set veth0-br up

# DEL (template)
ip -n <namespace> link delete veth0     # deleting one end removes the pair
# + release the IP in the plugin's IPAM store (plugin-specific)
  • Handshake (per CNI spec): config file + plugin-binary directory are configured on the node → Pod created → JSON prepared (pod name, ns, container ID, network config) → binary run with ADD → result JSON with IP → on deletion run with DEL. Standardised spec ⇒ any CNI works by dropping binaries + config into the right directories (§9.10).
  • Plugins ship an IPAM component and one of several datapaths: Linux bridge, VXLAN overlay, host-gw (routing), BGP routing, eBPF. Choose by cluster scale, performance and ops model.

[!WARNING] Exam traps / remember:

  • Model = unique Pod IP + same-node + cross-node reachability + no NAT. Kubernetes doesn't pick the IP range — the CNI does.
  • Same-node traffic works via the bridge; cross-node needs routes / overlay / BGP — usually the CNI's job.
  • CNI verbs: ADD, DEL (and CHECK, see §9.12). Config arrives as JSON on stdin.

9.10 CNI in kubernetes

Source: KodeKloud notes

Responsibilities

  • Per CNI spec: the component that creates the network namespace must also identify the network and invoke the plugin with the config.
  • In Kubernetes: Pod netns created via the container runtime (containerd / CRI-O); after that the configured CNI plugin attaches the Pod: veth pairs, attach to host bridge/overlay, assign IP, routes, NAT rules if requested.
  • Common plugins: Flannel, Calico, Weave, Cilium, Canal, …
  • Modern nodes run containerd as the CRI runtime (Docker historically used containerd internally).

Node paths

PathHoldsCheck
/opt/cni/binPlugin binaries (bridge, flannel, host-local, portmap, …)ls /opt/cni/bin
/etc/cni/net.dNetwork config (.conf, or .conflist for chained plugins) — says which plugin + network to usels /etc/cni/net.d
$ ls /opt/cni/bin
bridge  dhcp  flannel  host-local  ipvlan  loopback  macvlan  portmap  ptp  sample  tuning  vlan  weave-ipam  weave-net  weave-plugin-2.2.1
$ ls /etc/cni/net.d
10-bridge.conf
  • Multiple config files → the lexically first (alphabetical, lowest number) applicable file is used.

Bridge plugin config

{
  "cniVersion": "0.2.0",
  "name": "mynet",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "10.22.0.0/16",
    "routes": [
      { "dst": "0.0.0.0/0" }
    ]
  }
}
FieldMeaning
cniVersionCNI spec version targeted
nameLogical network name
typePlugin binary to run from /opt/cni/bin (bridge, flannel, calico…)
bridgeHost bridge interface (cni0)
isGatewayBridge gets an IP and acts as the Pods' gateway
ipMasqSNAT (iptables masquerade) for Pod egress to non-cluster destinations
ipam.typeAllocator: host-local (state kept on the node) or dhcp (external server)
ipam.subnetCIDR Pod IPs are drawn from
ipam.routesRoutes added in the Pod netns (0.0.0.0/0 = default route)

Pod start sequence

CNI ADD flow: kubelet → runtime creates sandbox/netns → picks /etc/cni/net.d config → execs /opt/cni/bin plugin → plugin wires veth/bridge and calls IPAM → result IP reported back Diagram (new): who does what when a Pod is attached to the network (step numbers match the list).

  1. kubelet asks the runtime to create the Pod sandbox and its network namespace.
  2. Config chosen from /etc/cni/net.d (first by lexical order).
  3. Plugin binary from /opt/cni/bin executed with the config + CNI environment variables.
  4. Plugin creates the veth, attaches it to bridge / VLAN / overlay, assigns IP (via IPAM), routes, NAT if requested.
  5. Plugin returns result JSON (IP, gateway, routes) to the runtime.
  6. Runtime returns sandbox status to kubelet; kubelet reports status.podIP to the API.

Troubleshooting checklist

  • Binaries present (and executable): ls /opt/cni/bin
  • Config present and sane: ls /etc/cni/net.d, cat the .conf/.conflist
  • Several config files → the first file in name order wins
  • cniVersion compatible with the plugin versions

[!CAUTION] Correction: the original says the kubelet reads /etc/cni/net.d and executes the plugin. Since dockershim's removal (v1.24) the container runtime (containerd / CRI-O) loads the CNI config and invokes the binaries; kubelet only calls the runtime over CRI (RunPodSandbox). The directories are configured in the runtime (e.g. the CRI cni section of /etc/containerd/config.toml), not on kubelet.

[!NOTE] Beyond the course: older videos find CNI settings with ps aux | grep kubelet → --network-plugin=cni, --cni-bin-dir=/opt/cni/bin, --cni-conf-dir=/etc/cni/net.d. Those kubelet flags were removed in v1.24; on current clusters just inspect the two directories. Weave Net is no longer maintained (Weaveworks shut down in 2024) — expect Flannel / Calico / Cilium in labs.

[!WARNING] Exam traps / remember:

  • Binaries → /opt/cni/bin; config → /etc/cni/net.d. Don't swap them.
  • The config's "type" must match a binary name in /opt/cni/bin.
  • "Which CNI is configured?" → read the file in /etc/cni/net.d (first alphabetically if several).

9.11 Lab Solution Explore CNI optional

Source: KodeKloud notes

Theory: §9.10.

#TaskCommand / techniqueAnswer
1Container runtime endpointps aux | grep -i kubelet | grep container-runtime → --container-runtime-endpoint=…unix:///var/run/containerd/containerd.sock
2Path of CNI plugin binariesls /opt/cni/bin/opt/cni/bin
3Plugin NOT availablecompare option list against ls /opt/cni/bincisco
4CNI configured for the clusterls /etc/cni/net.d/ → cat /etc/cni/net.d/10-flannel.conflistflannel (+ portmap chained)
5Binary run after a Pod/container is createdeach "type" in the conflist, in order/opt/cni/bin/flannel, then /opt/cni/bin/portmap
# Q1 kubelet process line (trimmed)
/usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf
  --config=/var/lib/kubelet/config.yaml --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock
  --pod-infra-container-image=registry.k8s.io/pause:3.9

# Q2 binaries
bandwidth bridge dhcp dummy firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan vrf
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    { "type": "flannel",
      "delegate": { "hairpinMode": true, "isDefaultGateway": true } },
    { "type": "portmap",
      "capabilities": { "portMappings": true } }
  ]
}
  • .conflist = plugin chain: runtime (via kubelet → CRI) calls each type in order when the Pod sandbox is created; portmap handles container port mappings (hostPort).
  • Runtime endpoint can also be confirmed in the kubelet systemd unit / /var/lib/kubelet/config.yaml.

[!WARNING] Exam traps / remember: a plugin named in the conflist but missing (or not executable) in /opt/cni/bin ⇒ Pod sandbox creation fails, Pods stuck ContainerCreating.

[!NOTE] Beyond the course: on newer kubeadm clusters the endpoint may not be on the command line — check containerRuntimeEndpoint in /var/lib/kubelet/config.yaml or /var/lib/kubelet/kubeadm-flags.env. --pod-infra-container-image is deprecated (the runtime decides the pause image).

9.12 ipam

Source: KodeKloud notes

Scope: Pod IPs (per-node bridge subnets and Pod addresses). Node IPs are out of scope (cloud provider / DHCP / infra tooling).

Who assigns Pod IPs

  • The CNI plugin — per the CNI spec it must allocate IPs and guarantee no duplicates. Kubernetes itself does not assign Pod IPs.

CNI plugin responsibilities (ADD/DEL/CHECK, params such as container id and netns, manage Pod IP assignment, return results in a fixed format) beside three nodes with v-net-0 bridges 10.244.1-3.0/24 on LAN 192.168.1.0

  • Plugin responsibilities: support ADD / DEL / CHECK; accept params (container ID, network namespace…); manage Pod IP assignment; return results in the spec's format.

Simple plugin flow

  • ADD: create veth pair, move container end into the netns, assign IP inside netns, add routes (default gateway), bring interfaces up.
  • DEL: remove the veth, release the allocated IP.
# ADD (script-style plugin)
ip link add veth-host type veth peer name veth-pod
ip link set veth-pod netns <container-netns>
ip -n <container-netns> addr add <IP>/<prefix> dev veth-pod
ip -n <container-netns> link set veth-pod up
ip -n <container-netns> route add default via <GATEWAY>
ip link set veth-host master cni0
ip link set veth-host up

# DEL
ip link del veth-host

[!CAUTION] Correction: the original pseudocode names the container end cni0 (same name as the bridge) and never moves it into the container netns before configuring it there. The block above moves the peer first; the bridge keeps the name cni0.

Where allocation state lives

Kubernetes doesn't care how — only that IPs are never handed out twice.

PatternUse caseExample
Local per-host storeSimple clusters, per-node subnetshost-local IPAM (file state on the node)
Central allocatorCluster-wide consistencyExternal IPAM service with an API
Standard IPAM plugin (recommended)Reuse tested allocation/persistence logicMain plugin delegates to host-local (or another IPAM binary)

Delegating to an IPAM plugin

  • Parse ipam.type (+ subnet, ranges, routes) from the CNI config in /etc/cni/net.d/.
  • Invoke that IPAM binary with the same CNI input JSON → returns IP + gateway + routes as JSON → configure the netns with it.
  • On DEL, call the IPAM plugin again to free the IP.
{
  "cniVersion": "0.2.0",
  "name": "mynet",
  "type": "net-script",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "subnet": "10.244.0.0/16",
    "routes": [ { "dst": "0.0.0.0/0" } ]
  }
}

(/etc/cni/net.d/net-script.conf — a custom net-script plugin delegating IPAM to host-local.)

Best practices (if writing your own allocator)

  • Prefer standard IPAM (host-local).
  • Own allocator: locks / atomic allocation for concurrency; persist state across restarts/crashes; periodically reconcile to find leaked IPs.
  • Know whether the network model wants per-host subnets (e.g. Flannel host-gw) or a cluster-wide allocator.
QuestionAnswer
Who assigns Pod IPs?The active CNI plugin
Where is state stored?Plugin-dependent: node-local store or external allocator
RecommendedStandard IPAM plugin, e.g. host-local
Where configured?ipam block of the CNI config in /etc/cni/net.d/

[!NOTE] Beyond the course: the video's Weave example — Weave Net allocates from 10.32.0.0/12 by default (≈1M addresses, split between peers/nodes); overridable with IPALLOC_RANGE (shows as ipalloc-range in its logs, see §9.14).

[!WARNING] Exam traps / remember: Pod IP range questions → look at the CNI (config ipam.subnet, CNI pod logs/args), not at the Service range. IPAM verbs come from CNI: ADD/DEL/CHECK.

9.13 Service Networking

Source: KodeKloud notes

Recap: each Pod gets a netns + veth + IP from its node's pod subnet; routing/overlay (VXLAN, IP-in-IP…) makes one cluster-wide Pod network (§9.9 route-table figure). Apps are normally reached via a Service: stable virtual IP + DNS name, cluster-wide (not tied to a node), reachable from any Pod unless NetworkPolicies block it.

Service types

TypeVisibilityUse
ClusterIP (simplest, most common)Inside the cluster onlyDBs, internal APIs / microservices
NodePortExternal via <any-node-IP>:<nodePort>Basic external access; same port opened on every node; extends ClusterIP
LoadBalancerExternal via cloud LBCloud provisions an external LB
ExternalNameDNS CNAMEMaps the Service name to an external DNS name

NodePort: port 30080 open on all three nodes, forwarding to service IPs 10.99.13.178/179 and Pod IPs 10.244.x.x

How Service IPs work

  • Services are virtual: no process, namespace or interface owns the IP; it's just an API object.
  • On creation the API server assigns a ClusterIP from the Service CIDR: kube-apiserver flag --service-cluster-ip-range (kubeadm default 10.96.0.0/12).
  • Per node: kubelet (Pods + CNI) and kube-proxy (watches Services/Endpoints via the API server, programs rules mapping ServiceIP:port → one backend PodIP:port).
  • Service add: API server assigns IP → kube-proxy on every node programs kernel rules (iptables/IPVS) to DNAT that IP:port to an endpoint; Service removed → rules removed.

kube-apiserver feeding kubelet and kube-proxy on three nodes; each node holds the same forwarding entry 10.99.13.178:80 → 10.244.1.2

kube-proxy proxy modes

ModeNotes
userspaceLegacy
iptablesDefault when unset; widely used
ipvsHigh-performance alternative

Set at start-up, e.g. kube-proxy --proxy-mode=iptables. The course also lists IPVS and historical userspace mode; supported modes depend on the binary version.

[!CAUTION] Correction (version drift): userspace mode was removed in v1.26. Current modes: iptables (Linux default), ipvs, and nftables (GA in v1.33). IPVS has since been deprecated upstream in favour of nftables. In kubeadm clusters the mode lives in the kube-proxy ConfigMap (mode: empty ⇒ iptables), not on a command line.

Worked example

kubectl get pods -o wide
# NAME  READY  STATUS   RESTARTS  AGE  IP          NODE
# db    1/1    Running  0         14h  10.244.1.2  node-1
kubectl get service
# NAME        TYPE       CLUSTER-IP      PORT(S)   AGE
# db-service  ClusterIP  10.103.132.104  3306/TCP  12h

# Service CIDR is an API-server flag
ps aux | grep kube-apiserver          # ... --service-cluster-ip-range=10.96.0.0/12

# kube-proxy's NAT rules (iptables mode), node-local
sudo iptables -t nat -L | grep db-service
# KUBE-SVC-XA5OGUC7YRHOS3PU  tcp -- anywhere 10.103.132.104 /* default/db-service: cluster IP */ tcp dpt:3306
# DNAT                       tcp -- anywhere anywhere       /* default/db-service: */ tcp to:10.244.1.2:3306
# KUBE-SEP-JBWCWHHQM57V2WN7  all -- anywhere anywhere       /* default/db-service: */
  • KUBE-SVC-* chain matches traffic to 10.103.132.104:3306; KUBE-SEP-* (service endpoint) chain holds the DNAT to 10.244.1.2:3306. Rules carry a comment with namespace/service, so grep <svc-name> works.

kube-proxy logs

$ sudo cat /var/log/kube-proxy.log
I0307 04:29:29.883941 1 server_others.go:140] Using iptables Proxier.
I0307 04:29:29.912037 1 server_others.go:174] Tearing down inactive rules.
I0307 04:29:30.027360 1 server.go:448] Version: v1.11.8
I0307 04:29:30.049773 1 conntrack.go:98] Set sysctl 'net/netfilter/nf_conntrack_max' to 131072
I0307 04:29:30.050701 1 proxier.go:294] Adding new service "default/db-service:3306" at 10.103.132.104:3306/TCP
  • Log shows the proxier in use and each Service/Endpoint add/remove. Location depends on how kube-proxy runs (systemd, static Pod, DaemonSet); raise verbosity with --v.
  • Rules missing? → kube-proxy running and connected to the API server? its logs + configured mode? Service and Endpoints exist (kubectl get svc, kubectl get endpoints)?

[!NOTE] Beyond the course: kubeadm cluster: kubectl -n kube-system logs <kube-proxy-pod> | grep -i proxier for the mode (no /var/log/kube-proxy.log; container logs are under /var/log/pods/kube-system_kube-proxy-*/ on the node); sudo iptables-save | grep <svc-name> is a quicker dump than -L.

[!NOTE] Beyond the course: NodePort default range 30000-32767 (kube-apiserver --service-node-port-range). Pod CIDR is set at install (kubeadm init --pod-network-cidr, kube-controller-manager --cluster-cidr); Service CIDR via kubeadm init --service-cidr (default 10.96.0.0/12), also on kube-controller-manager --service-cluster-ip-range. kubectl get endpoints is deprecated in favour of kubectl get endpointslices (v1.33+). In nftables mode inspect with sudo nft list ruleset.

[!WARNING] Exam traps / remember:

  • Service range = kube-apiserver --service-cluster-ip-range (/etc/kubernetes/manifests/kube-apiserver.yaml). Pod range = CNI / --cluster-cidr. They must not overlap (e.g. Pods 10.244.0.0/16, Services 10.96.0.0/12).
  • Test Services at their IP and port with curl/nc. iptables/nftables rules handle that tuple; IPVS may bind ClusterIPs to kube-ipvs0 and answer ping. Ping alone does not prove backend reachability.
  • kube-proxy runs on every node; no kube-proxy rules ⇒ Services unreachable from that node.
  • Course API-server process example: --authorization-mode=Node,RBAC --service-cluster-ip-range=10.96.0.0/12. The second flag defines the Service IP pool; authorization mode controls API permissions, not the pool.

9.14 Lab Solution Service Networking optional

Source: KodeKloud notes

Theory: §9.13.

#QuestionCommand / techniqueAnswer
1Node network rangekubectl get nodes -o wide (INTERNAL-IP 192.6.10.10) → ip addr on the node: inet 192.6.10.10/24 … eth0 → derive network from IP + mask192.6.10.0/24
2Pod IP rangeCNI is Weave: kubectl get pods -n kube-system, kubectl logs weave-net-rbx4p -n kube-system → ipalloc-range:10.244.0.0/1610.244.0.0/16
3Service IP range--service-cluster-ip-range in /etc/kubernetes/manifests/kube-apiserver.yaml10.96.0.0/12
4Number of kube-proxy Podskubectl get pods -n kube-system → count kube-proxy-*2
5kube-proxy proxy typekubectl logs kube-proxy-125qk -n kube-system → "Using iptables Proxier"iptables
6How kube-proxy runs on every nodekubectl get all --all-namespaces / kubectl get ds -n kube-system → daemonset.apps/kube-proxy (node selector kubernetes.io/os=linux)DaemonSet
# Fast versions
kubectl get nodes -o wide
ip addr show eth0                                   # interface holding the node InternalIP
grep service-cluster-ip-range /etc/kubernetes/manifests/kube-apiserver.yaml
kubectl -n kube-system logs <weave-pod> -c weave | grep ipalloc-range
kubectl -n kube-system logs <kube-proxy-pod> | grep -i proxier
kubectl -n kube-system get ds
  • /24 ⇒ hosts .1–.254, network x.x.x.0/24.
  • DaemonSet = one Pod on each node matching its node selector / tolerations — that's why the kube-proxy count equals the node count (also true for the CNI DaemonSet, weave-net here).

[!NOTE] Beyond the course: other ways to get the Pod range — grep cluster-cidr /etc/kubernetes/manifests/kube-controller-manager.yaml, kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' (per-node slices, only if node CIDR allocation is enabled), or the CNI's own config (/etc/cni/net.d/*, Flannel's kube-flannel-cfg ConfigMap net-conf.json). The CNI's setting is authoritative: Weave ignores node podCIDR.

[!WARNING] Exam traps / remember: multi-container CNI Pods (Weave: weave, weave-npc) — pass -c <container> or you may read the wrong log. Don't confuse node network (ip addr), Pod range (CNI) and Service range (apiserver flag).

Add after the 9.14 Q2 row: "Weave log lines to match: Command line options: ... ipalloc-range:10.244.0.0/16, plus adding entry 10.244.0.0/16 to weaver-no-masq-local (also confirms the range)."

9.15 DNS in kubernetes

Source: KodeKloud notes

Prereqs (§9.3): what DNS does, host / nslookup / dig, record types A and CNAME, domain hierarchy. Node hostnames/IPs (e.g. node1.kubecluster.org → 192.168.1.11) are managed by external DNS — out of scope; this is about in-cluster Pod ↔ Service ↔ Pod resolution. Kubernetes deploys a built-in DNS server (CoreDNS) by default.

Service records

  • Create Service web-service (ClusterIP 10.107.37.188) → DNS gets an A record web-service → ClusterIP. Node placement of client/backend Pods is irrelevant.
  • Same namespace: short name works. Other namespace: append the namespace.
curl http://web-service                         # client in same namespace as the Service
curl http://web-service.apps                    # Service in namespace "apps"
curl http://web-service.apps.svc
curl http://web-service.apps.svc.cluster.local  # FQDN, always unambiguous

Naming hierarchy

  • <service>.<namespace>.svc.<cluster-domain>; cluster domain (root) default cluster.local (changeable at cluster creation).
  • Each namespace is a subdomain; all Services are grouped under subdomain svc; all Pod records under pod.
NameUse whenResolves to
web-serviceclient in the same namespaceClusterIP
web-service.appsclient in another namespaceClusterIP
web-service.apps.svcexplicit svc subdomainClusterIP
web-service.apps.svc.cluster.localfully qualifiedClusterIP
  • Short names work because Pod resolvers carry search suffixes <ns>.svc.cluster.local, svc.cluster.local, cluster.local (§9.16).

DNS records table: web-service / apps / svc / cluster.local → 10.107.37.188; 10-244-2-5 / apps / pod / cluster.local → 10.244.2.5, with the cluster.local → svc|pod → apps → name tree

Pod records (optional)

  • Not created by default in many CoreDNS configs; when enabled: hostname = Pod IP with dots → dashes, under pod subdomain, keeping namespace + cluster domain.
curl http://10-244-2-5.apps.pod.cluster.local   # Pod 10.244.2.5 in namespace apps
TopicNotes
DefaultService A records exist; Pod records usually off (less DNS load)
Cluster domaincluster.local by default; changeable at creation
Pod-record useDebugging / legacy workflows; not a replacement for Services

[!CAUTION] Correction: kubeadm's default CoreDNS Corefile does include pods insecure, so on kubeadm clusters (incl. exam clusters) <a-b-c-d>.<ns>.pod.cluster.local resolves out of the box. "Disabled" is the CoreDNS kubernetes plugin default when pods is omitted.

[!NOTE] Beyond the course: an ExternalName Service yields a CNAME; a headless Service (clusterIP: None) returns the Pod IPs as A records instead of a ClusterIP, and StatefulSet Pods get <pod-name>.<headless-svc>.<ns>.svc.cluster.local. Named ports get SRV records _<port>._<proto>.<svc>.<ns>.svc.cluster.local.

[!WARNING] Exam traps / remember:

  • Cross-namespace access needs at least <svc>.<ns>; the bare name only resolves in the client's own namespace.
  • Pod records use the IP with dashes, not the Pod name, and need the pod subdomain (use at least <a-b-c-d>.<ns>.pod; the cluster.local search suffix can complete it; FQDN is safest).
  • Order is service → namespace → svc → cluster.local: <svc>.<ns>.svc.cluster.local (not <svc>.svc.<ns>…).

Add to the 9.15 Exam traps callout: "- Pod records (pods insecure) add one DNS entry per Pod and extra load on CoreDNS. Enable them only for a clear need such as debugging or legacy workflows; use Services for discovery."

[!CAUTION] Correction: the source’s “full FQDN required” rule is too strict: <a-b-c-d>.<ns>.pod can resolve via the usual cluster.local search suffix. Bare IP-with-dashes or <a-b-c-d>.<ns> does not add the missing pod component.

9.16 CoreDNS in Kubernetes

Source: KodeKloud notes

Why a DNS server instead of /etc/hosts

  • Could add entries to every Pod's /etc/hosts, but thousands of Pods churn → impractical. Instead: central DNS server; every Pod's /etc/resolv.conf points at it; new Services (and optionally Pods) get records automatically.
# /etc/hosts style (format: IP  name)
10.244.2.5   web
10.244.1.5   test

# /etc/resolv.conf in each Pod
nameserver 10.96.0.10

[!CAUTION] Correction: the original lists /etc/hosts entries as web 10.244.2.5 (name first). The file format is IP first, then hostname(s), as above.

CoreDNS deployment

  • DNS server runs as Pods in kube-system; CoreDNS is the default since v1.12 (before: kube-dns).
  • Deployed as a Deployment (→ ReplicaSet), normally 2 replicas for redundancy.
  • Each Pod runs the coredns binary with config file /etc/coredns/Corefile, supplied by ConfigMap coredns (edit it to change behaviour; reload picks up changes).
# /etc/coredns/Corefile (from ConfigMap kube-system/coredns)
.:53 {
  errors
  health

  kubernetes cluster.local in-addr.arpa ip6.arpa {
    pods insecure
    upstream
    fallthrough in-addr.arpa ip6.arpa
  }

  prometheus :9153

  forward . /etc/resolv.conf

  cache 30

  reload
}
DirectivePurpose
.:53Serve all zones on port 53
errors / healthError logging / health endpoint
kubernetes cluster.local in-addr.arpa ip6.arpa { … }Kubernetes integration; cluster.local = cluster domain (+ reverse zones)
↳ pods insecureCreate dashed-IP Pod records (10-244-2-5…); off unless set
↳ upstreamResolve external names (e.g. ExternalName targets) via upstream servers
↳ fallthrough in-addr.arpa ip6.arpaPass unanswered reverse (PTR) lookups to the next plugin
prometheus :9153Metrics endpoint
forward . /etc/resolv.confAnything not cluster-internal (e.g. www.google.com) → nameservers in the CoreDNS Pod's /etc/resolv.conf (inherited from the node)
cache 30Cache answers 30 s
reloadAuto-reload when the ConfigMap changes

[!CAUTION] Correction: upstream inside the kubernetes block is obsolete in current CoreDNS (ignored/removed); upstream resolution is done by forward. A current kubeadm Corefile also has ready, loop, loadbalance, ttl 30 and health { lameduck 5s }.

How Pods find CoreDNS

  • CoreDNS exposed by Service kube-dns in kube-system (ClusterIP often 10.96.0.10; ports 53/UDP, 53/TCP, 9153/TCP).
  • kubelet writes each Pod's /etc/resolv.conf from its config clusterDNS (DNS Service IP) and clusterDomain (cluster.local) — in /var/lib/kubelet/config.yaml on kubeadm nodes.
# Pod /etc/resolv.conf (Pod in namespace default)
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
grep -A1 clusterDNS /var/lib/kubelet/config.yaml   # clusterDNS: / - 10.96.0.10 (YAML list)
grep clusterDomain /var/lib/kubelet/config.yaml    # clusterDomain: cluster.local
  • Search domains make web-service, web-service.default, web-service.default.svc all expand to web-service.default.svc.cluster.local (tried in order).
  • No pod search suffix → use at least <a-b-c-d>.<ns>.pod (with cluster.local in the search list); FQDN is safest.
  • Unresolvable in-cluster → forwarded to upstream (node's) nameservers.

Service name to backend Pod: client queries kube-dns (CoreDNS) for web-service.apps, gets the ClusterIP, then connects; node iptables rules programmed by kube-proxy DNAT to the Pod Diagram (new): DNS answers name → ClusterIP; kube-proxy's rules then turn ClusterIP → Pod IP (§9.13).

ComponentRoleWhere
CoreDNSWatches the API; serves Service (optionally Pod) recordsDeployment/ReplicaSet in kube-system
kube-dns ServiceStable ClusterIP = cluster DNS serverkube-system
kubeletWrites Pod /etc/resolv.conf (nameserver + search)each node
coredns ConfigMapHolds the Corefilekube-system

[!NOTE] Beyond the course: test resolution from a throwaway Pod: kubectl run dns-test --image=busybox:1.28 --rm -it --restart=Never -- nslookup web-service.apps (busybox 1.28: newer busybox nslookup is unreliable). Pod resolv.conf also has options ndots:5.

[!WARNING] Exam traps / remember:

  • Service is named kube-dns even though the Pods are CoreDNS — kubectl get svc -n kube-system kube-dns.
  • Change DNS behaviour → kubectl -n kube-system edit cm coredns (not the Deployment); restart (kubectl -n kube-system rollout restart deploy coredns) if you can't wait for reload.
  • Wrong clusterDNS in kubelet config ⇒ Pods on that node can't resolve anything.

9.17 Lab Solution Explore DNS optional

Source: KodeKloud notes

Theory: §9.15, §9.16. Setup: alias k=kubectl.

#QuestionCommand / techniqueAnswer
1DNS solutionk get pods -n kube-system → coredns-* PodsCoreDNS
2Number of DNS Podssame listing (coredns-74ff55c5b-9lxcv, …-kgtwz)2
3Service for CoreDNSk get svc -n kube-systemkube-dns (ClusterIP, 53/UDP,53/TCP,9153/TCP)
4DNS server IP Pods useClusterIP of kube-dns (ends up in Pods' /etc/resolv.conf)10.96.0.10
5Corefile location in the Podk describe pod coredns-74ff55c5b-9lxcv -n kube-system → Args: -conf /etc/coredns/Corefile; Mounts /etc/coredns from config-volume (ro)/etc/coredns/Corefile
6How the Corefile is passed insame describe → Volumes: config-volume type ConfigMap, Name corednsConfigMap coredns
k get pods -n kube-system
k get svc -n kube-system
k describe pod <coredns-pod> -n kube-system      # Args + Mounts + Volumes
k get configmap coredns -n kube-system -o yaml   # view the active Corefile (data.Corefile)
  • Corefile shows the forwarding rules and the cluster domain (kubernetes cluster.local … → root domain cluster.local).

[!NOTE] Beyond the course: the describe output in the course also shows a coredns-token-xxxx Secret volume — that's pre-v1.24. Current clusters show a projected kube-api-access-xxxxx volume instead (no auto-created token Secret).


9.18 Ingress

Source: KodeKloud notes

Why Ingress (the my-online-store scenario)

  • Storefront wear app = Deployment. MySQL = Pod behind a ClusterIP Service mysql-service, so it is reachable only inside the cluster.
  • wear-service as a NodePort (e.g. 30080): users would browse http://<node-ip>:30080. Scaling pods works because the Service load-balances across them.
  • Production needs things NodePort alone doesn't give you:
    • a DNS name pointing at one external IP instead of node IPs
    • standard ports 80/443 instead of 30000+
    • path- or host-based routing for several apps (/watch, watch.myonlinestore.com)
    • TLS terminated and certificates managed in one place
  • On a cloud, Service type: LoadBalancer provisions a cloud LB, and a NodePort is still created underneath. Giving each Service its own LB means more LBs to pay for, plus another LB in front to route by path (/apparel → LB-1, /video → LB-2) and to hold TLS. That is costly and spread across places.
  • Ingress = one HTTP(S) entry point inside the cluster. It routes by host/path to many Services and terminates TLS. It is configured with Kubernetes objects and exposed externally once.

Per-Service cloud LBs: wear/video LoadBalancer Services (30080/30082), each with a GCP LB, plus another LB for /apparel and /video path routing and SSL

Controller vs resource

PieceWhat it is
Ingress controllerThe actual reverse proxy / L7 load balancer (NGINX, Traefik, Contour, HAProxy, GCE (GCP L7 LB), Istio). Runs as a Deployment or DaemonSet. It watches the API for Ingress objects and reconfigures itself (routing, LB, TLS, rewrites).
Ingress resourceDeclarative rules (host/path → Service:port) plus TLS config. Pure configuration.
  • No Ingress controller ships with Kubernetes. Ingress objects do nothing until you deploy one. NGINX is the one used in course examples and labs.
  • Flow: deploy controller → expose it (NodePort/LoadBalancer, one-time) → create Ingress resources → controller enforces them. New routes and certs only require Ingress changes.
  • A controller needs:
    • a Deployment (or DaemonSet)
    • a Service exposing it (NodePort/LoadBalancer)
    • an optional ConfigMap for settings
    • a ServiceAccount + RBAC, so it can watch Ingresses, Services, Endpoints, Secrets and Pods

Ingress traffic flow: client to controller Service, controller pod, backend Services Diagram (new): external traffic → controller Service → controller pod (which watches Ingress objects) → backend Service by host/path, with unmatched requests going to the default backend.

NGINX ingress controller (simplified, as in course)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginx-ingress
  template:
    metadata:
      labels:
        name: nginx-ingress
    spec:
      serviceAccountName: nginx-ingress-serviceaccount   # needs RBAC (Role/ClusterRole + bindings)
      containers:
      - name: nginx-ingress-controller
        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
        args:
        - /nginx-ingress-controller
        - --configmap=$(POD_NAMESPACE)/nginx-configuration   # ConfigMap holding nginx settings
        env:                                   # controller needs its own pod name/namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        ports:
        - name: http
          containerPort: 80
        - name: https
          containerPort: 443
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configuration          # may start empty
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress
spec:
  type: NodePort                     # or LoadBalancer on cloud
  selector:
    name: nginx-ingress
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount

[!CAUTION] Correction: the quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0 image is long obsolete. Current ingress-nginx images are registry.k8s.io/ingress-nginx/controller:<version>, and the project's official manifest or Helm chart creates all the RBAC, Services and the IngressClass for you. The hand-written manifest above is for understanding the components only.

Ingress resource: three routing patterns

PatternUse whenExample
Default backendAll traffic goes to one ServicedefaultBackend: wear-service
Path-basedOne host, several apps under different pathsmy-online-store.com/wear, /watch
Host-basedSeparate subdomain per appwear.my-online-store.com, watch.my-online-store.com

1. Default backend only

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wear
spec:
  defaultBackend:              # everything goes here
    service:
      name: wear-service
      port:
        number: 80
kubectl create -f ingress-wear.yaml
kubectl get ingress
# NAME           CLASS   HOSTS   ADDRESS   PORTS   AGE
# ingress-wear           *       <none>    80      2s
  • Requests that match no host or path go to the default backend. Setups often deploy a small default-http-backend Service that serves a friendly 404 page.

2. Path-based (one rule, many paths)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wear-watch
spec:
  rules:
  - http:                       # no host = any host (*)
      paths:
      - path: /wear
        pathType: Prefix
        backend:
          service:
            name: wear-service
            port:
              number: 80
      - path: /watch
        pathType: Prefix
        backend:
          service:
            name: watch-service
            port:
              number: 80
kubectl describe ingress ingress-wear-watch
# Default backend:  <default>
# Rules:
#   Host  Path    Backends
#   *     /wear   wear-service:80
#         /watch  watch-service:80
# Events: Normal CREATE ... nginx-ingress-controller  Ingress default/ingress-wear-watch created

Path-based: /wear → apparel app, /watch → video app, /eat and /listen → 404 default backend

3. Host-based (one rule per host)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wear-watch
spec:
  rules:
  - host: wear.my-online-store.com     # matched against the HTTP Host header
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: wear-service
            port:
              number: 80
  - host: watch.my-online-store.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: watch-service
            port:
              number: 80
  • Rules are per host. Each rule can contain several paths, for example wear. → /, /returns, /support and watch. → /, /movies, /tv. Anything else (listen., eat., drink.) falls through to the default backend (404).

Ingress rules: one rule per host (wear, watch), each with several paths; everything else to a 404 backend

TLS

  • Put the cert and key in a Secret of type kubernetes.io/tls, then reference it in spec.tls. TLS can be per host (recommended) or global, depending on what the controller supports.
  • cert-manager can obtain and renew certificates from Let's Encrypt automatically.
spec:
  tls:
  - hosts:
    - wear.my-online-store.com
    secretName: wear-tls-secret        # Secret type kubernetes.io/tls, same namespace as the Ingress
  rules: ...

[!NOTE] Beyond the course: create the TLS Secret:

kubectl create secret tls wear-tls-secret --cert=tls.crt --key=tls.key

Best practices

  • Run one controller, or one per team/namespace if needed. Expose it once and manage everything else through Ingress resources.
  • Protect cert Secrets with RBAC and namespace scoping.
  • pathType: choose Prefix or Exact. Regex and advanced matching depend on the controller, so check its docs.
  • Troubleshoot with kubectl describe ingress and the controller pod logs.

[!NOTE] Beyond the course (exam-relevant):

  • pathType is required in networking.k8s.io/v1. Values: Exact (exact path only), Prefix (matches by /-separated element, so /foo matches /foo and /foo/bar but not /foobar), ImplementationSpecific (controller decides; used with regex paths).
  • IngressClass: set spec.ingressClassName: nginx to choose the controller. kubectl get ingressclass lists the classes. A class annotated ingressclass.kubernetes.io/is-default-class: "true" is used when the field is omitted. The old kubernetes.io/ingress.class annotation is deprecated.
  • Imperative creation (--rule syntax host/path=svc:port[,tls=secret]):
    kubectl create ingress ingress-wear-watch -n app-space \
      --class=nginx \
      --rule="wear.my-online-store.com/*=wear-service:80" \
      --rule="/watch=watch-service:80" \
      --annotation=nginx.ingress.kubernetes.io/rewrite-target=/ \
      --default-backend=default-http-backend:80
    # path ending in * -> pathType Prefix; plain path -> pathType Exact
    # "host/path=svc:port,tls=my-cert" adds a tls entry
  • ingress-nginx (the community controller) retired in March 2026; maintenance and security fixes ended. Migrate deployments to a maintained controller/Gateway API (§9.21). This concerns the controller; the Ingress API remains stable. See the upstream retirement notice. Follow the controller provided by any lab/exam task.

[!WARNING] Exam traps / remember:

  • Ingress object without a controller = accepted but ignored. Check kubectl get pods -A for the controller first.
  • The backend service.port.number is the Service port, not the containerPort.
  • Ingress and its backend Services, and its TLS Secret, must be in the same namespace. An Ingress cannot point at a Service in another namespace.
  • Host rules match the Host header. A rule with no host matches every host (*).
  • v1 backend syntax is service.name / service.port.number. The old serviceName/servicePort fields (v1beta1) were removed in 1.22.

9.19 Lab Solution Ingress Networking 1 optional

Source: KodeKloud notes

Setup: the controller is pre-deployed in ingress-nginx, the apps are in app-space, and the Ingress is ingress-wear-watch. Theory is in §9.18.

Drill

TaskCommand / techniqueAnswer / gotcha
Nodes / cluster versionkubectl get nodessingle controlplane node, v1.23.0 (ROLES control-plane,master)
Where are the deployments?kubectl get deploy -Anothing in default. app-space: default-backend, webapp-video, webapp-wear. ingress-nginx: ingress-nginx-controller (may initially show 0/1 READY; wait before testing)
Ingress controller namespace / namekubectl get pods -A (or get deploy -A)namespace ingress-nginx, deployment ingress-nginx-controller
Which namespace are the apps in?kubectl get deploy -Aapp-space
Find all Ingresseskubectl get ingress -Aingress-wear-watch in app-space, HOSTS *, ADDRESS 10.96.152.118
Host configured?kubectl describe ingress ingress-wear-watch -n app-space → Rules → Host* = all hosts
Backend for /wear, /watchdescribe → Rules/wear → wear-service:8080, /watch → video-service:8080 (pod endpoints shown in brackets)
Annotations presentdescribe → Annotationsnginx.ingress.kubernetes.io/rewrite-target: /, nginx.ingress.kubernetes.io/ssl-redirect: "false"
Default backenddescribe → Default backenddefault-http-backend:80. Unmatched paths show its 404 page
Change /watch → /streamkubectl edit ingress ingress-wear-watch -n app-space, change path: /watch to /streamtest with curl http://<ingress-ip>/stream
Add food app at /eatkubectl get deploy,svc -n app-space (new webapp-food, food-service 8080/TCP), then add a path entry in the same Ingress/eat → food-service:8080
Payment app in its own namespacekubectl get deploy -n critical-space, kubectl get svc -n critical-spacedeploy webapp-pay, svc pay-service port 8282 (the pod listens on 8080; describe shows 10.244.0.11:8080)
Expose /paycreate a new Ingress in critical-space (see below)an Ingress can't reference a Service in another namespace
/pay returns 404kubectl logs -n critical-space <webapp-pay-pod> → "GET /pay HTTP/1.1" 404the app only serves /. Fix with the rewrite-target: / annotation

Edited ingress-wear-watch (final state)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wear-watch
  namespace: app-space
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /       # strip the matched path before proxying
    nginx.ingress.kubernetes.io/ssl-redirect: "false"   # no forced HTTP->HTTPS redirect
spec:
  rules:
  - http:
      paths:
      - path: /wear
        pathType: Prefix
        backend:
          service:
            name: wear-service
            port:
              number: 8080
      - path: /stream                 # was /watch
        pathType: Prefix
        backend:
          service:
            name: video-service
            port:
              number: 8080
      - path: /eat                    # new food app
        pathType: Prefix
        backend:
          service:
            name: food-service
            port:
              number: 8080
curl http://10.96.152.118/wear
curl http://10.96.152.118/stream
curl http://10.96.152.118/eat
  • When you kubectl edit, you will also see a status.loadBalancer.ingress[].ip block. It is controller-managed, so leave it alone, and don't include status in manifests you write yourself.

Payment Ingress in critical-space

kubectl create ingress ingress-pay -n critical-space --rule="/pay=pay-service:8282"
kubectl get ingress -n critical-space
kubectl describe ingress ingress-pay -n critical-space   # /pay -> pay-service:8282 (10.244.0.11:8080), Annotations: <none>

Initial result is 404: the app log shows "GET /pay HTTP/1.1" 404, because the full path was forwarded. Fix:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-pay
  namespace: critical-space
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /   # /pay -> / at the backend
spec:
  rules:
  - http:
      paths:
      - path: /pay
        pathType: Prefix          # kubectl create ingress made it Exact
        backend:
          service:
            name: pay-service
            port:
              number: 8282        # Service port, not the pod's 8080
curl http://10.96.152.118/pay
kubectl logs -n critical-space <webapp-pay-pod>    # now: "GET / HTTP/1.1" 200

rewrite-target and regex

  • With rewrite-target: /, everything matched by /pay reaches the backend as /.
  • To keep subpaths (/pay/foo → /foo), add nginx.ingress.kubernetes.io/use-regex: "true", use a regex path with a capture group such as /pay/(.*), and set rewrite-target: /$1. Annotation behaviour differs between ingress-nginx versions, so test it.

[!NOTE] Beyond the course:

  • The canonical ingress-nginx pattern is path: /pay(/|$)(.*) with rewrite-target: /$2. It handles both /pay and /pay/foo; /pay/(.*) does not match bare /pay. Regex paths should use pathType: ImplementationSpecific, because newer ingress-nginx versions reject regex characters in Prefix/Exact paths when strict path validation is on.
  • You can add an annotation without opening an editor: kubectl annotate ingress ingress-pay -n critical-space nginx.ingress.kubernetes.io/rewrite-target=/. You can also set it at creation time with kubectl create ingress ... --annotation=nginx.ingress.kubernetes.io/rewrite-target=/.
  • Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) in describe is a display quirk of older kubectl, which looked in kube-system when spec.defaultBackend was unset. Newer kubectl prints <default> (§9.18). The controller uses its own default backend.

[!TIP] Create each Ingress in the same namespace as its app. That avoids cross-namespace problems and lets each team own its routes. Debug 404s by reading the backend pod's log to see which path actually arrived.


9.20 Lab Solution Ingress Networking 2 optional

Source: KodeKloud notes

Goal: deploy an NGINX ingress controller from scratch, expose it, and route /wear and /watch to the apps in app-space. To start with, only app-space pods (default-backend, webapp-video, webapp-wear) and system pods exist. Theory is in §9.18.

Drill

#TaskCommandGotcha
1Namespace for the controllerkubectl create namespace ingress-spacekeeps controller objects and RBAC scoped together
2ConfigMap the controller expectskubectl create configmap nginx-configuration -n ingress-spaceempty is fine. The name must match the --configmap arg
3ServiceAccount + RBAC (pre-created)kubectl get roles,rolebindings -n ingress-space, kubectl describe role ingress-role -n ingress-spaceingress-role bound via ingress-role-binding (see rules below)
4Deploy the controllerfix the provided YAML, then kubectl create -f ingress-controller.yamla parse error such as yaml: line 36: mapping values are not allowed in this context means bad indentation or stray characters
5Expose the controllerkubectl expose deploy ingress-controller -n ingress-space --name ingress --port=80 --target-port=80 --type NodePortyou get a random nodePort (e.g. 80:32741/TCP). If the task gives a specific one, kubectl edit svc ingress -n ingress-space and set spec.ports[0].nodePort
6Check app Serviceskubectl get svc -n app-spacedefault-http-backend 80, video-service 8080, wear-service 8080
7Create the Ingresskubectl create ingress ingress-wear-watch -n app-space --rule="/wear=wear-service:8080" --rule="/watch=video-service:8080"must be in app-space, where the Services are
8Verifykubectl get ingress -n app-space, kubectl describe ingress -n app-spacebackends should list pod endpoints, e.g. wear-service:8080 (10.244.0.6:8080)
9Browser shows ERR_TOO_MANY_REDIRECTSkubectl logs <nginx-ingress-pod> -n ingress-space shows repeated "GET /watch HTTP/1.1" 308308 = HTTP→HTTPS redirect. The log includes upstream [app-space-video-service-8080] (<namespace>-<service>-<port>), confirming the route matched. If the controller forces redirects but the intended endpoint serves HTTP, add ssl-redirect: "false" (below); inspect controller and backend behavior to identify the loop
10App logs show nothingkubectl logs <webapp-pod> -n app-space shows only the Flask startup banner, Running on http://0.0.0.0:8080/the request never reached the app, so the problem is at the ingress layer

ingress-role rules (from kubectl describe role):

ResourceResource namesVerbs
configmapsanyget, create
configmapsingress-controller-leader-nginxget, update
endpointsanyget
namespacesanyget
podsanyget
secretsanyget

Controller Deployment (corrected)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-controller
  namespace: ingress-space
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginx-ingress
  template:
    metadata:
      labels:
        name: nginx-ingress
    spec:
      serviceAccountName: ingress-serviceaccount
      containers:
      - name: nginx-ingress-controller
        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
        args:
        - /nginx-ingress-controller
        - --configmap=$(POD_NAMESPACE)/nginx-configuration
        - --default-backend-service=app-space/default-http-backend   # <ns>/<svc> for unmatched requests
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        ports:
        - name: http
          containerPort: 80
        - name: https
          containerPort: 443

Final Ingress (after fixing the redirect loop)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-wear-watch
  namespace: app-space
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /        # apps serve at /
    nginx.ingress.kubernetes.io/ssl-redirect: "false"    # stop the 308 HTTP->HTTPS loop
spec:
  rules:
  - http:
      paths:
      - path: /wear
        pathType: Exact          # what `kubectl create ingress` generates for a plain path
        backend:
          service:
            name: wear-service
            port:
              number: 8080
      - path: /watch
        pathType: Exact
        backend:
          service:
            name: video-service
            port:
              number: 8080
kubectl edit ingress ingress-wear-watch -n app-space    # add the two annotations
# then browse http://<node-ip>:<nodePort>/wear and /watch
  • Hitting <node-ip>:<nodePort>/ with no matching path shows the default backend's "OOPS!! 404" page. That is expected, and it proves the controller is serving. If /wear or /watch themselves show that 404, inspect kubectl describe ingress -n app-space (host/path/pathType/backends), then controller and app logs.

[!TIP] ssl-redirect: "false" only switches off the forced redirect. It does not provide TLS. If the app needs HTTPS, add a tls block with a cert Secret instead (§9.18).

[!NOTE] Beyond the course:

  • Add both annotations in one command: kubectl annotate ingress ingress-wear-watch -n app-space nginx.ingress.kubernetes.io/ssl-redirect="false" nginx.ingress.kubernetes.io/rewrite-target=/.
  • To set a fixed nodePort without editing afterwards: kubectl expose ... --dry-run=client -o yaml > svc.yaml, add nodePort: 30080 under the port, then kubectl apply -f svc.yaml.
  • Exact matches only /wear, not /wear/ or /wear/x. Use Prefix (or --rule="/wear*=...") if subpaths must work.

[!WARNING] Exam traps / remember:

  • The controller's ConfigMap name and namespace must match --configmap=$(POD_NAMESPACE)/nginx-configuration.
  • --default-backend-service takes namespace/service.
  • kubectl expose --port is the Service port and --target-port is the container port (80 for nginx).
  • Read logs in order: controller log first (does the request arrive? which status code?), then the app log (which path arrived?).

[!CAUTION] Correction: the course’s controller image quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0 predates networking.k8s.io/v1 and cannot serve this corrected example on current clusters. Use the controller installed/provided for the task; a complete deployment also needs its matching RBAC, flags and configuration. The YAML above is retained as a historical lab example (§9.18).

9.21 Introduction to Gateway API 2025 Updates

Source: KodeKloud notes

Ingress limitations that Gateway API addresses

  • Single-resource ownership: one Ingress often carries the routes of many teams' Services (e.g. one object with both wear. and watch. host rules). Only one team can safely edit it, which makes it a point of conflict in multi-tenant clusters.
  • Annotation sprawl: advanced behaviour is written as controller-specific annotations that Kubernetes cannot validate and that don't port between controllers. Examples: SSL redirect, canary weights, CORS, header rewrites, rate limiting.
  • Mostly HTTP(S) only.

Gateway API model (L4 + L7, controller-agnostic)

A Kubernetes (SIG Network) project that splits the single Ingress into role-specific resources:

RoleResourcePurpose
Infrastructure providerGatewayClassWhich controller implementation (controllerName) manages Gateways of this class (like IngressClass)
Cluster operatorGatewayAn instance of a class: the actual entry point. Holds listeners (protocol/port), TLS settings, and which routes may attach (allowedRoutes)
Application developerRoutes: HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRouteHost/path matching, filters, backends. Attach to a Gateway via parentRefs

Gateway API roles: infrastructure providers → GatewayClass, cluster operators → Gateway, application developers → HTTPRoute/TCPRoute

Gateway API object references and role ownership Diagram (new): Gateway API object references: HTTPRoute → Gateway (parentRefs) → GatewayClass (gatewayClassName) → controller; the Gateway listener points at a TLS Secret (certificateRefs); the route splits traffic with backendRefs weights.

Canonical objects (gateway.networking.k8s.io/v1)

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: example-class
spec:
  controllerName: example.com/gateway-controller   # which controller reconciles this class
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: example-gateway
spec:
  gatewayClassName: example-class                  # bind to the class above
  listeners:
  - name: http
    protocol: HTTP
    port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example-httproute
spec:
  parentRefs:                                      # attach to the Gateway
  - name: example-gateway
  hostnames:
  - "www.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix                           # PathPrefix | Exact | RegularExpression
        value: /login
    backendRefs:                                   # target Service + port
    - name: example-svc
      port: 8080

TLS: annotations vs structured listeners

Ingress approach: spec.tls plus controller annotations for the redirect.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-app
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  tls:
  - hosts:
    - secure.example.com
    secretName: tls-secret
  rules:                          # required: rules or defaultBackend
  - host: secure.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: secure-svc
            port:
              number: 80

Gateway approach: TLS is declared on the listener.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: secure-gateway
spec:
  gatewayClassName: example-gc
  listeners:
  - name: https
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate              # terminate TLS at the gateway
      certificateRefs:
      - kind: Secret
        name: tls-secret
    allowedRoutes:
      kinds:
      - kind: HTTPRoute            # only HTTPRoutes may attach to this listener
  • TLS termination is explicit and needs no annotations. The Gateway decides which route kinds may attach to each listener.

Traffic splitting (canary)

Ingress (NGINX-only annotations):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"    # 20% to this Ingress's backend
spec:
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-v2
            port:
              number: 80

Gateway API (weight is a native field, so any conformant controller implements it):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: split-traffic
spec:
  parentRefs:
  - name: app-gateway
  rules:
  - backendRefs:
    - name: app-v1
      port: 80
      weight: 80                  # 80% of requests
    - name: app-v2
      port: 80
      weight: 20                  # 20% of requests

Filters: header manipulation (CORS example)

Ingress needs per-controller annotations, which are Ingress metadata.annotations only:

ControllerAnnotations used
NGINXnginx.ingress.kubernetes.io/enable-cors: "true", cors-allow-methods: "GET, PUT, POST", cors-allow-origin: "https://allowed-origin.com", cors-allow-credentials: "true" (all with the nginx.ingress.kubernetes.io/ prefix)
Traefiktraefik.ingress.kubernetes.io/headers.customresponseheaders: | with a multi-line list of Access-Control-Allow-Origin: '*', -Methods: GET,POST,PUT,DELETE,OPTIONS, -Headers: Content-Type,Authorization, -Credentials: true, Access-Control-Max-Age: 3600

Gateway API uses a portable filters block on the route rule:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: cors-route
spec:
  parentRefs:
  - name: my-gateway
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api
    filters:
    - type: ResponseHeaderModifier       # add/set/remove response headers
      responseHeaderModifier:
        add:
        - name: Access-Control-Allow-Origin
          value: "*"
        - name: Access-Control-Allow-Methods
          value: "GET,POST,PUT,DELETE,OPTIONS"
        - name: Access-Control-Allow-Headers
          value: "Content-Type,Authorization"
        - name: Access-Control-Allow-Credentials
          value: "true"
        - name: Access-Control-Max-Age
          value: "3600"
    backendRefs:
    - name: api-service
      port: 80
  • Any conformant controller that implements ResponseHeaderModifier honours this the same way.

Ingress vs Gateway API

AreaIngressGateway API
OwnershipOne resource, usually owned by one teamGatewayClass/Gateway (infra) separate from Routes (apps)
TLSspec.tls + controller annotationsStructured tls on Gateway listeners
Advanced behaviourController-specific annotationsFirst-class filters and route fields
Traffic splittingAnnotations (controller-specific)weight on backendRefs (portable)
ProtocolsMainly HTTP(S)HTTP, HTTPS/TLS, TCP, UDP, gRPC
PortabilityLow for advanced featuresHigh, controller-agnostic

Implementations

Gateway API is implemented by (course slide, statuses at the time):

  • GA: Amazon EKS, Azure Application Gateway for Containers, Contour, Easegress, Envoy Gateway, Gloo Gateway, GKE, HAProxy Kubernetes Ingress Controller, Istio, Kong Ingress Controller, Kong Gateway Operator, Kuma, NGINX Gateway Fabric, Traefik Proxy, WSO2 APK
  • Beta: Apache APISIX, Cilium, Flomesh Service Mesh, LoxiLB, STUNner
  • Alpha / preview / WIP: HAProxy Ingress (alpha), Emissary-Ingress (alpha), Avi Kubernetes Operator (tech preview), ngrok (preview), Tyk (work in progress)
  • Also listed with no status: Acnodal EPIC, Airlock Microgateway, HashiCorp Consul, LiteSpeed Ingress Controller

[!NOTE] Beyond the course (exam-relevant):

  • Gateway API is not built into the cluster. Its CRDs are installed separately (standard-install.yaml from the kubernetes-sigs/gateway-api releases), and you also need a controller. Check with kubectl get crd | grep gateway.
  • Inspect with kubectl get gatewayclass (ACCEPTED), kubectl get gateway -A (ADDRESS, PROGRAMMED), kubectl get httproute -A, and kubectl describe httproute <name> (status shows whether the route was Accepted by its parent and whether ResolvedRefs succeeded).
  • Channels: GatewayClass, Gateway, HTTPRoute and GRPCRoute are in the standard channel (v1). TCPRoute and UDPRoute are experimental (v1alpha2), so use the apiVersion the installed CRD serves (kubectl explain tcproute).
  • allowedRoutes.namespaces.from defaults to Same. An HTTPRoute in a different namespace from the Gateway won't attach unless the listener sets from: All or from: Selector. The route's parentRefs also needs namespace:.
  • Cross-namespace backendRefs or certificateRefs need a ReferenceGrant in the target namespace.
  • parentRefs[].sectionName attaches a route to one specific listener. weight defaults to 1. A rule with no matches matches PathPrefix /.
  • Filter support: RequestHeaderModifier and RequestRedirect are Core; URLRewrite, RequestMirror and ResponseHeaderModifier are Extended. Check the controller’s conformance for the Gateway API version in use; see HTTPRoute filters.
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: pay-route
    spec:
      parentRefs:
      - name: example-gateway
        sectionName: http              # attach to the "http" listener only
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /pay
        filters:
        - type: URLRewrite             # /pay/x -> /x at the backend
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: pay-service
          port: 8282
    ---
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: https-redirect
    spec:
      parentRefs:
      - name: example-gateway
        sectionName: http
      rules:
      - filters:
        - type: RequestRedirect        # 301 every HTTP request to HTTPS
          requestRedirect:
            scheme: https
            statusCode: 301
  • The Traefik annotation in the course table is illustrative. Traefik v2+ configures headers through Middleware CRDs, which reinforces the course's point that annotations aren't portable.
  • The nginx canary annotations only work alongside a primary Ingress with the same host/path that points at the stable Service (app-v1).

[!WARNING] Exam traps / remember:

  • Chain of references: Route parentRefs → Gateway; Gateway gatewayClassName → GatewayClass; GatewayClass controllerName → controller. A broken link anywhere means no traffic.
  • Route path match types are PathPrefix / Exact / RegularExpression, not Ingress's Prefix.
  • backendRefs takes name + port directly (no service: wrapper like Ingress).
  • Listener protocol: HTTPS with tls.mode: Terminate needs certificateRefs pointing at a TLS Secret.

[!CAUTION] Correction: the course’s secure-app Ingress has spec.tls alone. The API requires rules or defaultBackend; the example above includes a routing rule.


← 08 Storage · Index · 10 Design and Install a Kubernetes Cluster →


Reference

Based on the KodeKloud Certified Kubernetes Administrator (CKA) course notes.

On this page

9.1 Networking IntroductionPrerequisite checklist (Linux networking basics)Section roadmap (each builds on the previous)9.2 Prerequisite Switching Routing Gateways CNI in kubernetesSwitching (same subnet)Routing and gateways (different subnets)Linux host as a routerPersistenceQuick referenceDNS / CoreDNS and namespaces (preview)9.3 Prerequisite DNSLocal resolution: /etc/hostsCentral DNS server: /etc/resolv.confLookup order: /etc/nsswitch.confDNS hierarchy and recursive resolutionRecord typesTesting tools9.4 Prerequisite Network NamespacesConceptCreate / inspectConnect two namespaces directly: veth pair ("virtual cable")Many namespaces: Linux bridge (virtual switch)Host ↔ namespacesNamespaces → outside (routing + forwarding + NAT)Outside → service in a namespaceQuick reference9.5 Prerequisite Docker NetworkingNetwork modesBridge internalsReachability and port publishing9.6 Prerequisite CNIWhy CNIResponsibilitiesPluginsDocker is the exception9.7 Cluster NetworkingNode requirementsRequired portsInspection commandsTroubleshooting checklist9.8 Lab Solution Explore Environment optional9.9 Pod NetworkingKubernetes pod networking model (requirements any solution must meet)Manual build on one node (what a CNI plugin automates)Inter-node routingWhere CNI fits9.10 CNI in kubernetesResponsibilitiesNode pathsBridge plugin configPod start sequenceTroubleshooting checklist9.11 Lab Solution Explore CNI optional9.12 ipamWho assigns Pod IPsSimple plugin flowWhere allocation state livesDelegating to an IPAM pluginBest practices (if writing your own allocator)9.13 Service NetworkingService typesHow Service IPs workkube-proxy proxy modesWorked examplekube-proxy logs9.14 Lab Solution Service Networking optional9.15 DNS in kubernetesService recordsNaming hierarchyPod records (optional)9.16 CoreDNS in KubernetesWhy a DNS server instead of /etc/hostsCoreDNS deploymentHow Pods find CoreDNS9.17 Lab Solution Explore DNS optional9.18 IngressWhy Ingress (the my-online-store scenario)Controller vs resourceNGINX ingress controller (simplified, as in course)Ingress resource: three routing patternsTLSBest practices9.19 Lab Solution Ingress Networking 1 optionalDrillEdited ingress-wear-watch (final state)Payment Ingress in critical-spacerewrite-target and regex9.20 Lab Solution Ingress Networking 2 optionalDrillController Deployment (corrected)Final Ingress (after fixing the redirect loop)9.21 Introduction to Gateway API 2025 UpdatesIngress limitations that Gateway API addressesGateway API model (L4 + L7, controller-agnostic)Canonical objects (gateway.networking.k8s.io/v1)TLS: annotations vs structured listenersTraffic splitting (canary)Filters: header manipulation (CORS example)Ingress vs Gateway APIImplementations