Virtualization basics
Normal Virtual machines virtualizes the underlying hypervisor to enable running multiple (OS + kernel) in parallel , here the hypervisor manages different VMs and each VM is a dedicated Machine in itself having its own kernel and OS, the hypervisor allocates memory and storage to each VM and schedules apps on cpu accordingly.. in dockerized container we virtualize the underlying OS… here we are using containers for virtualization , every container shares the host machine OS and kernel.. each container is isolated each container has its own PID namespace, meaning inside container PID starts from 1 , from outside PID is usual maps to real system id, NOTE : Hypervisor is only needed for true virtualization (os + kernel) it does not come installed https://www.youtube.com/watch?v=Sz2ayy2NomY
Hypervisor (Bare metal)
Hardware
↓
Hypervisor
↓
VMs (each has its own OS + kernel)
Hypervisor (hosted / software )
Hardware
↓
Host OS (with its kernel)
↓
Hypervisor (as a program)
↓
VMs (each has its own OS + kernel)
Docker stack
Hardware
↓
Linux Kernel (cgroups feature lives here)
↓
Docker / container runtime
↓
Containers (process groups)
How container virtualization happens
container is basically isolated process group with its own namespaces (PID , Network and FileSystem) , so basically each container has its own PID namespace → separate process tree , Mount namespace → separate filesystem view , Network namespace → own IP, ports so suppose there are 2 container running on a machine and inside each container 2 process running containerA -> ngnix , worker containerB --> python , java Each container will have its own process tree so the host kernel will see all the processIds like this Inside each container PID starts from 1
PID 1000 nginx (PID 1 inside Container A)
└─ PID 1001 worker (PID 2 inside Container A) (child process of ngnix)
PID 2000 python (PID 1 inside Container B)
└─ PID 2001 java (PID 2 inside Container B) (child process of python (PID = 1 = main process)
How resources are controlled across containers
cgroups → basically these are linux kernel level feature that limits usage of processes. WE saw that multiple isolated container can be run over a kernel , to make sure that one container doesn’t jeopardizes the other cGroup limits the usage of resources based on PID namespaces or this group of process can only use upto 1 GB of RAM We can limit the number of resources a container uses by docker run --memory=512m --cpus=1 nginx , so kernel creates a cgroup and basically puts all underlying processess under it…(with these limits) In our above example we can put a cgroup around containerA so it will make sure container A doesnt use more RAM or CPU then specified..
Why Docker mattered
Basically before docker all the deployment used to happen normally automted via jenkins jobs… basically jenkins checks out your code install dependencies and push the jar to the server where it is deployed and then you can use your app Code → pom.xml → JAR → run on server This used to run fine as all the required scripts were present on jenkins… In this setup managing custom install scripts and configs is painful… everything is dependent on scripts which is managed by devops team…. so suppose now you are upgrading to java17 on dev but on prod its still java8 you have to tell devops team to manually update the script and re-install dependencies. There was no easy way to check what dependencies are installed on a running jar — you have to check the deployment configuration Scaling is painful as the time took to spin up a new server is long and there are no easy way to autoscale you have to manage that by yourself..… even rollback was painful (restore to previous dependencies — refer to previous deployment configs) If you have to reproduce production behaviour … you have to request for all the manual scripts written and install dependencies yourself … too much hassel , basically there is no easy way to replicate prod in 1-2 step.. The whole kill the server …. rollback …. spin up old server was manual… Now if you kill a server a new one just comes up automatically. The classic runs on my machine but not on production problem… So its more like the complete orchestration + containerd process replaced the manual deployment process …
Kubernetes orchestration
So now we can have multiple containers running on our machine ….. if we only have 1-2 container it is easier to manage now imagine we have 10 machine and 1000 container… we need to have a system to manage all those stuffs like : Which machine runs which container? What if a container crashes? How do you update without downtime? what happens if there is traffic spike we need more containers… How do containers talk to each other?
Kubernetes is basically an orchestration tool over containers… It manages our containers across machines
Hardware (many machines)
↓
OS + Kernel (each machine)
↓
Container runtime (Docker/containerd)
↓
Kubernetes
↓
Your applications (containers)
You want to scale your application so you want 3 containers of your application running… you manually configure which machine should you make the containers run — Instead k8s does this for you… you just have to tell him — Run 10 containers for my app — It will figure out which machine and containers.. Some of the power of k8s include scheduling (basic orchestrator role) — schedules which container runs on which machine self healing — container died — new one spins up auto scaling — creates /reduces containers based on incoming traffic load.. rollout-depolyment — deploy new application with no downtime K8s does not manage container directly it manages pods Pod = wrapper around 1 or more containers (usually contains only one container but can contain multiple containers… all containers under a pod share same network and storage)
Replica placement across nodes
It depends on k8s it usually tries to spread deployment across multiple nodes for reliability but not guaranteed. K8s is kind of like abstraction over the underlying machine or node… you dont have to care about nodes anymore just mention the application requirment rest k8s will manage on its own..
Where Kubernetes sits
Kubernetes does two things at two levels: Cluster --> Basically collection of nodes (VMs) Cluster level (across machines) → control plane Node level (on each machine) → uses container runtime
Cluster level:
Control Plane (central brain)
↓
Node 1 Node 2 Node 3
↓ ↓ ↓
kubelet kubelet kubelet
↓ ↓ ↓
containerd containerd containerd
↓ ↓ ↓
containers containers containers
Control plane and data plane
Control PLANE (decision making) This is the brain or the entry point (kubectl talks to this) basically decides which node runs which pod etcd : Stores cluster state (desired + current)
Data PLANE (execution) Actually runs the container Kubelet --> Talks to the control plane … ensures pod are running Containerd or container runtime --> actually runs the container
Lifecycle of a POD state : replicas: 2 Control PLANE : reacts Controller → "Need 2 pods" Scheduler → "Place Pod1 on Node1, Pod2 on Node2" Data PLANE : executes Node1 kubelet → start Pod1 Node2 kubelet → start Pod2
What if pod1 dies ? Controller detects mismatch ↓ New pod scheduled ↓ Kubelet starts it
------— More Ques
Why containerd replaced Docker in Kubernetes
Because it never needed this…. K8s needed something that can run the container for that purpose only containerd was sufficient.. Even docker uses containerd underneath to run the container. Docker has whole lot of other stuff k8s does not need. Docker CLI ↓ Docker Engine ↓ containerd ↓ runc ↓ containers
What Docker Engine actually handled
DockerEngine was only responsible for creating and managing containers.. The namespace isolation is provided by the Linux kernel (does the heavy work) docker run nginx ↓ Docker Engine ↓ containerd ↓ runc ↓ Linux kernel
- namespaces
- cgroups
Modern deployment flow
Docker → build & package image Kubernetes → run & manage it at scale containerd/runc → actually execute it
Code → Dockerfile → Docker build → Image --> (push to registry)
Example docker build -t my-app:v1 . docker push my-app:v1
Image is stored in (DockerHub / ECR)
Kubernetes step app.yaml --> deployment file kubectl apply -f app.yaml
state : replicas: 2 image: my-app:v1
Control plane receives the request kubectl ↓ API Server ↓ etcd (stores desired state)
"Need 2 pods" --> Controller creates 2 pods
Scheduler decides placement Pod1 → Node A Pod2 → Node B
Node (kubelet) takes over NodeA : kubelet sees → "I must run this pod"
Kubelet talks to container runtime (via CRI) kubelet → containerd (via CRI)
Image Handling containerd:
- pulls image (my-app:v1) if not present
- prepares filesystem layers
containerd calls runc containerd → runc
Runc (does the real work)
- creates namespaces (using kernel) PID → isolated process tree Mount → isolated filesystem Network → separate IP stack
- applies cgroup (cpu and memory limits)
- exec → your app (PID 1 inside container)
- final state Your app is now:
- just a process
- isolated by kernel
- running on a node
OS vs kernel
Kernel : Core part of OS that talks with the underlying hardware , manage processes. OS : Kernel
- system libraries
- utilities (bash, ls, etc.)
- UI (optional)
- services Ubuntu = Linux kernel + GNU tools + packages
In containers: All containers share the same kernel But each has its own user space (OS-like environment) That’s why containers are lightweight
Example : When you run ls • ls = part of OS (user space) • it asks kernel via system call: → “give me files” --> returns only those files visible by container ( kernel applies container’s mount namespace)