KCNA — Kubernetes and Cloud Native Associate
From VMs to Containers — What We Gave Up and What We Gained
One-line summary
A container is not a "lightweight virtual machine." It confines a process with two Linux features (namespaces and cgroups) while sharing the kernel, which is why it is fast but its isolation is weaker than a VM's. This one sentence is the root of the entire KCNA scope.
Why this was needed
A virtual machine imitates hardware. A whole extra guest OS boots, booting takes tens of seconds, and the kernel image and system files consume disk and memory. Deploying one service meant deploying an operating system along with it.
The problem was not the "heaviness" but that the unit of deployment and the unit of execution were misaligned. What the developer made was an application, but what was handed to operations was a VM image. "It works on my laptop" is born in that gap.
Containers flipped the question. The kernel is already on the host, so why not just use it and change only the world the application sees? The kernel features for changing that "world that is seen" already existed.
How it works
Namespace: what does it see
| namespace | What it hides | How it looks inside a container |
|---|---|---|
| PID | Process list | My process appears as PID 1 |
| NET | Network interfaces and ports | Each container has its own IP and port space |
| MNT | Mount tree | The image's filesystem appears as the root |
| UTS | Hostname | The container name becomes the hostname |
| IPC | Shared memory and semaphores | Other containers' IPC is not visible |
| USER | UID/GID mapping | root inside, an unprivileged user outside |
cgroup: how much does it use
If a namespace cuts the "field of view," a cgroup cuts the "share." It limits CPU time, the memory ceiling, block IO, and the number of PIDs per group. If a process exceeds the memory ceiling, the kernel's OOM killer kills a process inside that group. Kubernetes' resources.limits.memory is ultimately pushed down to this cgroup value.
Neither one alone makes a container. With only namespaces, a neighboring container eats all the memory and you die together; with only cgroups, everyone's processes and files are visible to each other.
OCI: who holds the standard
In the early days, "container = Docker." When a single vendor holds the format and the way of running, the ecosystem cannot grow, so the OCI (Open Container Initiative) split out three specifications.
- Image Spec: The format of layers and the manifest. That is why an image built with
docker buildruns on any runtime. - Runtime Spec: The convention for taking an unpacked filesystem (rootfs) and
config.jsonand starting a process. - Distribution Spec: The HTTP API for talking to a registry.
The runtime has two layers
kubelet --(CRI/gRPC)--> containerd --(OCI Runtime Spec)--> runc --> 리눅스 커널
containerd is the high-level runtime. It pulls and unpacks images, manages snapshots, and tracks the lifecycle. runc is the low-level runtime. It reads config.json, creates the namespaces and cgroups, execs the process, and then disappears immediately. While the container is running, no runc process exists.
The design principles of containerd boil down to four: simplicity (do one thing well), plugin-based, OCI-compatible, and a gRPC API. Because every feature is a plugin, you can swap the snapshotter from overlayfs to devmapper.
12-factor: the shape of an app that fits well in a container
12-factor predates containers, but today it is effectively read as "the conditions for an app that runs well in containers." Three items come up often in KCNA.
- Config from the environment: The image must be the same in every environment, and what differs must be injected values.
- Stateless processes: State lives in an attached backing service, and a container must be able to die at any time.
- Disposability: Start fast, and when SIGTERM arrives, clean up gracefully and exit.
What it looks like in practice
The author's homelab cluster originally used cri-dockerd as its runtime. When rebuilding the cluster, it moved to containerd 1.7.27, and this was not a matter of taste but removing one layer. On the cri-dockerd path it was kubelet → cri-dockerd → dockerd → containerd → runc, with two more intermediate hops, and using containerd directly it ends at kubelet → containerd → runc. It does the same job with two fewer processes.
One more thing. After the rebuild, that homelab set its kernel to 6.14. This is what "containers share the kernel" actually means in operations: the node's kernel version is the upper bound of the features containers can use. A stack that depends on kernel features, such as eBPF-based networking, will not even turn on if the kernel is too old.
What to read next
This module has no lab. In the next reading, you look at the CRI, the interpreter between kubelet and containerd, and in the module after that you connect to a real cluster and start by poking at kubectl api-resources.