If you've ever felt like Kubernetes is a pile of magic YAML files, this article is for you. We're going to build understanding from the ground up — starting with plain Linux, moving to containers, and then climbing all the way to Pods, Nodes, Deployments, Services, and Ingress. By the end, you won't just know the names of these pieces — you'll know why they exist.
Why Start With Linux?
Kubernetes didn't invent containers. It's an orchestrator sitting on top of Linux features that already existed. If you understand the Linux primitives first, every Kubernetes concept afterward becomes "oh, that's just automating this thing I already understand" instead of new magic to memorize.
Two Linux kernel features make containers possible:
1. Namespaces — Isolation
A namespace makes a process think it's the only thing running on the machine. Linux has several kinds:
- PID namespace – the process sees itself as PID 1, even though the host sees it as PID 48213.
- Network namespace – gives the process its own network interfaces, IP address, and routing table, separate from the host.
-
Mount namespace – gives the process its own view of the filesystem (its own
/). - UTS namespace – lets the process have its own hostname.
- IPC namespace – isolates inter-process communication (shared memory, semaphores).
- User namespace – lets a process be "root" inside the container but be an unprivileged user on the host.
A container is just a regular Linux process that has been put inside a bundle of these namespaces so it believes it's alone on the machine.
2. Control Groups (cgroups) — Resource Limits
Namespaces isolate what a process can see. Cgroups control how much it can use — CPU shares, memory limits, disk I/O, network bandwidth. This is how you can say "this container gets max 512Mi of RAM" and have the kernel actually enforce it.
Takeaway: Container = Linux process + namespaces (isolation) + cgroups (resource limits) + a filesystem bundle (the image). Nothing more mystical than that.
Container Images
An image is a read-only template for creating containers. It's built in layers — each instruction in a Dockerfile (FROM, RUN, COPY, etc.) creates a new layer stacked on top of the previous one using a union filesystem (like OverlayFS).
Layer 4: COPY . /app <- your app code
Layer 3: RUN npm install <- dependencies
Layer 2: RUN apt-get update <- OS packages
Layer 1: FROM node:20-alpine <- base OS
Why layers matter:
- Layers are cached — if only your app code changes, Docker/containerd reuses the lower layers instead of rebuilding everything.
- Layers are shared across images — ten images built
FROM node:20-alpineshare that base layer on disk instead of duplicating it.
Images live in registries (Docker Hub, GitHub Container Registry, AWS ECR, etc.). When Kubernetes needs to run a container, it pulls the image from a registry to the Node first.
Container Runtime
This is the piece that actually takes an image and turns it into a running, namespaced, cgroup-limited process. Kubernetes doesn't talk to Docker directly anymore — it talks through a standard interface called CRI (Container Runtime Interface).
The common runtime stack today:
kubelet
│ (talks CRI protocol)
▼
containerd (high-level runtime: manages images, pulls, storage)
│
▼
runc (low-level runtime: actually calls Linux syscalls to create
namespaces/cgroups and start the process)
runc is doing the literal work we described in the Linux section: calling clone() with namespace flags, setting up cgroups, and exec()-ing your process. Everything above it is orchestration.
Pods — The Smallest Deployable Unit
Here's a question that trips people up: why doesn't Kubernetes just schedule containers directly? Why the extra layer of a "Pod"?
Answer: because real applications are often more than one process that need to share resources tightly — think of a main app container plus a logging sidecar that both need to read the same files and talk over localhost.
A Pod is a group of one or more containers that share:
-
The same network namespace — all containers in a Pod share one IP address and can reach each other over
localhost. Container A on port 8080 and container B on port 9090 in the same Pod just calllocalhost:8080/localhost:9090. - Optionally, storage volumes — a Pod can define a volume that multiple containers mount.
- The same lifecycle — they're scheduled together, and they die together.
How is the shared network namespace actually implemented? Kubernetes silently creates a hidden "pause" container for every Pod. This container does nothing except hold open the network namespace. Every other container in the Pod joins that namespace instead of creating its own. That's the real implementation detail behind "Pods share networking."
Pod (shares one network namespace + IP: 10.244.1.7)
├── pause container (holds the namespace, does nothing)
├── app container (localhost:8080)
└── sidecar container (localhost:9090, e.g. log shipper)
This is also why the Pod, not the container, is the atomic unit Kubernetes schedules, scales, and replaces.
Nodes — Where Pods Actually Run
A Node is a worker machine (VM or bare metal) running the components needed to host Pods. Every Node runs:
- kubelet – the agent that talks to the control plane, receives instructions ("run this Pod"), and tells the container runtime to do it via CRI. It also continuously reports Pod health back up.
- container runtime – containerd + runc, as covered above.
-
kube-proxy – maintains network rules on the Node (via
iptablesorIPVS) so that traffic sent to a Service IP gets routed to the correct backend Pod, even though the Pod could be on a completely different Node.
Think of the kubelet as a very obedient local supervisor: the control plane never SSHs into a Node to start something — it just updates a desired state, and kubelet on each Node notices and reconciles reality to match it.
The Control Plane — The Brain
This is the part that decides what should run where, and it's separate from the Nodes that actually run your workloads.
-
kube-apiserver – the front door. Every single interaction with the cluster —
kubectl, the scheduler, controllers, kubelets — goes through the API server. It validates requests and is the only component that talks directly to storage. - etcd – a distributed key-value store that holds the entire cluster state (every object, every desired configuration). If etcd is healthy, the cluster's "memory" is safe.
- kube-scheduler – watches for Pods with no Node assigned yet, and decides which Node they should run on, based on resource availability, affinity rules, taints/tolerations, etc.
- kube-controller-manager – runs the reconciliation loops. Example: the ReplicaSet controller constantly checks "do I have 3 Pods running as declared? If not, create more."
The core pattern to internalize: you declare desired state (via YAML → API server → etcd). Controllers continuously watch for drift between desired state and actual state, and correct it. Kubernetes never "runs a command once" — it enforces a state forever.
Deployments
A Deployment is a controller that manages ReplicaSets, which in turn manage Pods.
Deployment
└── ReplicaSet (v1)
├── Pod
├── Pod
└── Pod
Why the extra ReplicaSet layer instead of Deployment managing Pods directly? Rolling updates. When you update the image in a Deployment, it doesn't touch existing Pods — it creates a new ReplicaSet with the new Pod template, scales it up gradually, and scales the old ReplicaSet down. This is how you get zero-downtime deploys and easy rollbacks (rolling back = pointing back at the old ReplicaSet).
Services — Solving the Pod IP Problem
Pods are mortal. They get killed and recreated constantly (crashes, scaling, node failures) — and every time, they get a new IP address. So how does anything reliably talk to "the backend"?
This is what a Service solves. A Service gets a stable virtual IP and DNS name, and uses labels and selectors to find which Pods should receive traffic.
selector:
app: my-backend
Any Pod labeled app: my-backend automatically becomes a backend for this Service — no matter how many times those Pods get replaced. kube-proxy on every Node maintains the routing rules that make "traffic to the Service IP" actually land on one of the healthy matching Pods.
Three Service types matter most:
- ClusterIP (default) – a virtual IP reachable only from inside the cluster. Good for internal service-to-service communication (e.g., frontend Pod → backend Service).
-
NodePort – opens a specific port (30000–32767 range) on every Node's own IP, forwarding it into the Service. Lets external traffic in via
<NodeIP>:<NodePort>, but it's clunky for production (you're exposing raw node IPs and non-standard ports). - LoadBalancer – asks the cloud provider (AWS/GCP/Azure) to provision an actual external load balancer that points at the Service. This is the standard way to expose something to the internet on a cloud cluster.
Port Forwarding vs Services
kubectl port-forward pod/my-pod 8080:80 is a developer debugging tool, not a production traffic mechanism. It opens a temporary tunnel from your local machine straight to a single Pod, bypassing Services and kube-proxy entirely. The moment you kill that terminal, the connection is gone. It's for "let me poke this one Pod directly while debugging" — never how real traffic reaches your app.
Ingress — HTTP Routing at the Edge
If Services (specifically LoadBalancer) already expose things externally, why do we need Ingress too?
Problem: if you have 10 HTTP microservices, giving each one its own cloud LoadBalancer means 10 expensive external load balancers, and no shared logic for things like TLS termination, host-based routing (api.example.com vs app.example.com), or path-based routing (/users vs /orders).
Ingress is a set of routing rules — "if the request comes in for api.example.com/users, send it to the users-service ClusterIP Service." But Ingress rules alone do nothing — you need an Ingress Controller (NGINX Ingress Controller, Traefik, etc.) running as Pods in your cluster that actually reads those rules and does the routing. The Ingress Controller is usually the one component that does get a single LoadBalancer Service, and everything else flows through it.
Internet
│
▼
LoadBalancer Service (1 external IP)
│
▼
Ingress Controller Pods (reads Ingress rules)
│
├── /users → users-service (ClusterIP) → users Pods
├── /orders → orders-service (ClusterIP) → orders Pods
└── api.example.com → api-service (ClusterIP) → api Pods
Putting the Whole Request Path Together
Here's the full journey of one HTTP request hitting your cluster, tying every piece above together:
- Request hits the cloud LoadBalancer, which forwards to the Ingress Controller Pods.
- Ingress Controller reads the Ingress rules and decides which internal Service should handle this path/host.
- It forwards the request to that Service's ClusterIP.
- kube-proxy's iptables/IPVS rules on the Node intercept traffic to that ClusterIP and redirect it to one of the matching Pods, chosen via the Service's label selector.
- Traffic lands inside the Pod's shared network namespace, reaching the actual container process — a Linux process running inside namespaces and cgroups, started by runc, pulled from an image by containerd, on instructions from the kubelet, which got that instruction from the API server, which persisted it from a Deployment you applied, which is stored durably in etcd.
That's the entire stack, front to back, in one sentence.
Closing Thoughts
Every Kubernetes concept exists to solve a concrete problem:
| Problem | Kubernetes Answer |
|---|---|
| Isolate processes on Linux | namespaces + cgroups |
| Package an app reproducibly | container images |
| Actually run a container | container runtime (containerd + runc) |
| Group tightly-coupled containers | Pods |
| Run Pods somewhere | Nodes (kubelet, kube-proxy, runtime) |
| Decide cluster-wide state | Control plane (API server, etcd, scheduler, controllers) |
| Zero-downtime updates & scaling | Deployments + ReplicaSets |
| Stable networking to mortal Pods | Services + labels/selectors |
| Smart HTTP routing at the edge | Ingress + Ingress Controller |
Once you see it this way, Kubernetes stops being "a huge list of YAML kinds to memorize" and becomes "a small number of real problems, each solved by one clean abstraction."
If this helped clarify things, I'll be following up with a deeper dive into Services and networking internals (ClusterIP vs NodePort vs LoadBalancer under the hood) — let me know in the comments what you'd like covered next.
Top comments (0)