Ever since module 2 we have been writing resources blocks in every Rutas Norte manifest — cpu: 250m, memory: 256Mi — without ever explaining what they mean exactly or where those numbers come from. The previous lesson even exposed them to the container with resourceFieldRef so that bookings-api could size itself. It is time to take them seriously, because they are the piece that decides which node each pod fits on, how much CPU it gets when there is competition and which process dies when memory runs short. And we are still carrying the last module 2 debt: no namespace has a quota, so right now a wrong deployment in rutas-norte-dev can eat the cluster capacity and leave the production pods with nowhere to go. In this lesson you will understand the complete resource model and you will put a ResourceQuota on each of the three environments.
Contents
- The resource model:
requestsandlimits - Units: millicores,
MiversusM - Who uses
requests: the scheduler - Who uses
limits: the kubelet and cgroups - Compressible CPU versus incompressible memory
- Diagnosing CPU throttling
- Diagnosing an
OOMKilled ephemeral-storageand disk eviction- Cluster overcommitment
- ResourceQuota: what it limits and how to read it
- The effect that forces you to declare resources
- The quotas for the three Rutas Norte environments
- How to choose sensible values
- The resource model:
requests and limits
requests and limitsEvery container in a pod can declare two numbers for each type of resource:
- name: api
image: registry.rutasnorte.example/bookings-api:2.5.0
resources:
requests: # what the container NEEDS GUARANTEED
cpu: 250m
memory: 256Mi
limits: # the CEILING it cannot exceed
cpu: "1"
memory: 512MiThe conceptual difference, which is the basis of everything:
requests |
limits |
|
|---|---|---|
| Meaning | "Reserve this for me" | "Do not let me go past this" |
| Who uses it | The scheduler, when picking a node | The kubelet, at runtime |
| When it acts | Once, when the pod is created | Continuously, for as long as the pod lives |
| If the node does not have it | The pod stays Pending |
— |
| If it is exceeded | It cannot be exceeded: it is guaranteed | CPU: throttling. Memory: OOMKilled |
| Affects the cluster bill | Yes: it is committed capacity | Indirectly |
| If omitted | Assumed to be 0 (dangerous) | Assumed unlimited (dangerous) |
A useful analogy, and one that fits Rutas Norte: think of a coach in the fleet. The request is the reserved seat: it is paid for and nobody else can take it, whether you use it or not. The limit is the maximum luggage allowance: you can carry less, but if you try to go over the maximum they stop you at the door.
And the two consequences to fix in your mind from the outset:
requestsis what "costs" in the cluster. A pod withrequests: 4Giblocks 4 GiB of node capacity even if its process uses 100 MiB. The scheduler treats that node as having 4 GiB less available for everything else.limitsreserves nothing. A pod withlimits: 8Gidoes not reserve 8 GiB; it simply will not be able to go past that if it tries to use them.
You can declare only requests, only limits, both or neither:
| Declaration | Effect |
|---|---|
Both, with requests < limits |
The usual case: a minimum guarantee with room to burst |
| Both, equal | Maximum predictability and maximum priority (you will see it in 03-05) |
Only limits |
Kubernetes copies the limit into the request. Careful: you reserve more than you think |
Only requests |
No ceiling: the container can consume the whole node |
| Neither | The worst case: no guarantee and no ceiling |
- Units: millicores,
Mi versus M
Mi versus MCPU
The CPU unit is the core (more precisely, a thread of execution: a vCPU in the cloud, a hyperthread on bare metal). It can be expressed in two ways:
| Written as | Meaning |
|---|---|
1 or 1000m |
A whole core |
500m |
Half a core |
250m |
A quarter of a core |
100m |
A tenth of a core |
0.5 |
Equivalent to 500m, but discouraged |
1m |
The minimum accepted |
The m stands for milli: 1000m = 1 core. The m form is preferred because it avoids floating-point errors and because 0.1 can suffer representation problems. Always write millicores.
CPU is fractional and elastic: if you ask for 250m you are not given a quarter of a physical processor but a quarter of the CPU time available in a time window. And if the node is idle, you can use more (up to your limit).
Memory
Memory is measured in bytes and accepts two families of suffixes that are not equivalent:
| Suffix | Base | Value | Example |
|---|---|---|---|
Ki |
2 | 1,024 | 512Ki = 524,288 bytes |
Mi |
2 | 1,048,576 | 256Mi = 268,435,456 bytes |
Gi |
2 | 1,073,741,824 | 1Gi = 1,073,741,824 bytes |
Ti |
2 | 2⁴⁰ | |
k |
10 | 1,000 | 512k = 512,000 bytes |
M |
10 | 1,000,000 | 256M = 256,000,000 bytes |
G |
10 | 1,000,000,000 | 1G = 1,000,000,000 bytes |
256M is 6.9% less memory than 256Mi. And 1G is 74 MiB less than 1Gi. In a memory limit, that 6.9% is exactly the difference between a container that just about copes and one that dies with OOMKilled under load.
Rule for Rutas Norte and for any serious project: always use Mi and Gi, never M or G. It is what every Kubernetes tool does and what anyone reading your manifest expects.
And a typo that costs dearly:
Kubernetes accepts it without complaint: m is a valid suffix (milli). The container gets a limit of half a byte and dies instantly. The symptom is a baffling CrashLoopBackOff. m only makes sense for CPU.
If you see that, you already know what the problem is.
- Who uses
requests: the scheduler
requests: the schedulerThe kube-scheduler you studied in module 1 has one job: pick a node for each new pod. And the decision is based exclusively on requests, not on actual usage.
flowchart TB
A["New pod:<br/>requests cpu=250m mem=256Mi"] --> B["FILTERING<br/>Which nodes have free ALLOCATABLE capacity?"]
B --> C{"node-1<br/>free: 200m CPU"}
B --> D{"node-2<br/>free: 1500m CPU"}
B --> E{"node-3<br/>free: 800m CPU"}
C -->|"does NOT fit"| F["Discarded"]
D -->|"fits"| G["SCORING"]
E -->|"fits"| G
G --> H["Chosen: the best score"]
H --> I["The pod is BOUND to the node<br/>spec.nodeName"]
The calculation the scheduler makes for each node is:
Two critical nuances:
Nuance 1: allocatable is not the node's total capacity. Kubernetes reserves a portion for the operating system and for its own components (kubeReserved, systemReserved, evictionHard).
Capacity:
cpu: 4
ephemeral-storage: 61202244Ki
memory: 8039484Ki
pods: 110
Allocatable:
cpu: 4
ephemeral-storage: 56403448552
memory: 7937084Ki
pods: 110In this minikube the difference is small, but on a managed cloud node it can be 10-15% of the memory. Always plan against Allocatable, not against Capacity.
Nuance 2: requests are summed, not actual usage. This is the source of the most common confusion. A node can be at 8% actual CPU usage and still reject a new pod because the sum of the requests of what is already assigned leaves no room.
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 2350m (58%) 5200m (130%)
memory 2816Mi (36%) 4608Mi (59%)
ephemeral-storage 0 (0%) 0 (0%)Read it carefully: the CPU Requests add up to 58% of allocatable, but the Limits add up to 130%. That is the overcommitment of section 9, and that note in brackets warns about it explicitly.
When there is no room on any node, the pod stays Pending:
kubectl get pods -n rutas-norte-dev
kubectl describe pod bookings-api-6d8f7c-abc -n rutas-norte-dev | tail -5NAME READY STATUS RESTARTS AGE
bookings-api-6d8f7c-abc 0/1 Pending 0 2m14s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m default-scheduler 0/3 nodes are available:
3 Insufficient memory. preemption: 0/3 nodes are available: 3 No preemption victims found.Insufficient memory with the pod in Pending always means the same thing: the requests do not fit. The fixes are lowering the requests (if they were inflated), adding a node, or waiting for the cluster autoscaler to do it for you.
A warning about inflated requests: it is tempting to ask for more "just in case". But every MiB of request you do not use is cluster capacity nobody can use and that you are paying for. In real clusters it is normal to find 30-40% of capacity committed and unused purely because of badly calibrated requests.
- Who uses
limits: the kubelet and cgroups
limits: the kubelet and cgroupsThe scheduler never sees limits. They are applied by the kubelet on the node, and not on its own: it asks the Linux kernel through cgroups (control groups), the mechanism that allows the resources of a set of processes to be limited.
We can see it from inside the container. With limits: cpu: "1" and memory: 512Mi on cgroups v2:
kubectl exec -n rutas-norte-dev deploy/bookings-api -- sh -c \
'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/cpu.weight'Let us decode the three lines, because they explain the whole behaviour:
memory.max = 536870912: that is exactly 512 MiB. It is a hard ceiling. If the process tries to allocate byte 536870913, the kernel fires the OOM killer.cpu.max = 100000 100000: these are quota and period in microseconds. It means "100,000 µs of CPU every 100,000 µs", that is, a whole core. Withlimits: cpu: 500mit would be50000 100000.cpu.weight = 10: it derives from the CPUrequest(250m). It is the relative weight with which the kernel scheduler shares out CPU time when there is competition. A container with twice therequestreceives twice the time.
That last line is important and little known: the CPU request is not only for the Kubernetes scheduler; it also determines the relative priority within the node. Two pods on the same node competing for CPU share the time in proportion to their requests.
- Compressible CPU versus incompressible memory
This is the most important distinction in the lesson and the one you really have to understand.
| CPU | Memory | |
|---|---|---|
| Type of resource | Compressible | Incompressible |
| Can be taken away live | Yes: just give it less time | No: what is allocated is allocated |
When the limit is exceeded |
Throttling | OOMKilled: the process dies |
| Consequence | The process runs slower | The container restarts and work in progress is lost |
| Visible in | container_cpu_cfs_throttled_seconds metrics |
kubectl describe pod, RESTARTS |
| Severity | High latency, timeouts | Lost requests, service outage |
CPU: throttling. If your container has limits: cpu: 500m and tries to use more, the kernel simply gives it no more CPU time in that 100 ms period. The process does not die: it waits. The visible effect is latency. For bookings-api this means that a request that took 80 ms starts taking 400 ms, and if the TIMEOUT_MS of web-store is set to 2000, with enough throttling timeout errors start appearing in the customer's browser.
Memory: death. There is no way to "throttle" memory. If a process has allocated 512 MiB and asks for more, the kernel cannot take anything back: either it gives it memory or it kills somebody. With limits: memory: 512Mi, it kills the process that went over, inside the container's cgroup. The container dies with exit code 137 and the kubelet restarts it according to the restartPolicy.
The practical design consequence:
Memory
limitshave to be calibrated carefully, because falling short kills the process. CPUlimitsare far less dangerous, but also far more debatable.
And here there is a genuine debate in the community that is worth knowing about: should you set CPU limits or not?
| In favour of a CPU limit | Against a CPU limit |
|---|---|
| It stops a runaway process degrading its neighbours | It throttles even when the node is idle: capacity is wasted |
| It makes performance predictable across environments | It can cause throttling during short start-up bursts |
| It lets you detect sooner that the application needs more | With threads, throttling affects the whole process, not just the guilty thread |
Required for the Guaranteed class (03-05) |
Many large teams deliberately omit them |
The Rutas Norte position, which is the recommendable one for a team that is starting out:
- CPU and memory
requests: always, on every container. Not negotiable. - Memory
limits: always. A pod with no memory limit can bring down the whole node and cause cascading evictions. - CPU
limits: yes indevandpre(to spot excessive consumption early) and generous inpro, between 2 and 4 times therequest, so that bank-holiday and school-holiday peaks can be absorbed.
- Diagnosing CPU throttling
Throttling is treacherous because it does not show up in any pod state. The pod is Running, READY 1/1, with no restarts, and it still responds badly.
The signal is in the cgroup itself:
usage_usec 184920331
user_usec 152014882
system_usec 32905449
nr_periods 918442
nr_throttled 214883
throttled_usec 41220984How to read it:
nr_periods: how many 100 ms periods have elapsed.nr_throttled: in how many of them the container used up its quota and was throttled.throttled_usec: total microseconds spent waiting.
The key indicator is the ratio:
Almost a quarter of the periods have been throttled. Interpretation:
| Ratio | Diagnosis |
|---|---|
| < 1% | Normal. Occasional bursts |
| 1-5% | Keep an eye on it. Acceptable in batch processes |
| 5-25% | A real latency problem. Raise the limit |
| > 25% | Serious. The limit is clearly set wrong |
A command to review every pod of a component:
for P in $(kubectl get pods -n rutas-norte-pro -l app=bookings-api -o name); do
echo "--- $P"
kubectl exec -n rutas-norte-pro "$P" -- sh -c \
'awk "/nr_periods|nr_throttled/ {print \$1, \$2}" /sys/fs/cgroup/cpu.stat'
done--- pod/bookings-api-7f4b8c9d6-2xkqp
nr_periods 918442
nr_throttled 214883
--- pod/bookings-api-7f4b8c9d6-8mvtr
nr_periods 917210
nr_throttled 198445The proper way to monitor this continuously is with metrics: the container_cpu_cfs_throttled_periods_total series from cAdvisor, which you will see in Monitoring with Prometheus. The command above is for a one-off diagnosis.
The fix: raise the CPU limit. If bookings-api has requests: 250m and limits: 500m with 23% throttling, raising the limit to "1" usually solves it at no real cost, because the limit reserves no capacity.
- Diagnosing an
OOMKilled
OOMKilledThe opposite case is loud and easy to identify:
NAME READY STATUS RESTARTS AGE
bookings-api-7f4b8c9d6-2xkqp 1/1 Running 4 (2m ago) 41m
bookings-api-7f4b8c9d6-8mvtr 0/1 OOMKilled 3 (18s ago) 41mThe detail is in describe:
Containers:
api:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Wed, 05 Aug 2026 23:12:04 +0200
Finished: Wed, 05 Aug 2026 23:14:41 +0200
Restart Count: 3
Limits:
cpu: 1
memory: 512Mi
Requests:
cpu: 250m
memory: 256MiThe three unmistakable clues: Reason: OOMKilled, Exit Code: 137 (which is 128 + 9, the SIGKILL signal) and a Restart Count that keeps growing.
Careful with Last State: it describes the previous container. If the pod is Running right now but has RESTARTS 4, Last State tells you why it died last time. It is the first thing to look at when facing a pod with restarts.
And a warning about kubectl logs: when the container restarts, the logs start from scratch. To see those of the dead container, which are the ones holding the clue:
2026-08-05T23:14:38.221Z INFO [pod=bookings-api-7f4b8c9d6-8mvtr] availability_query
route=Bilbao-Santander dates=2026-08-14..2026-08-17 results=8412
2026-08-05T23:14:40.887Z WARN [pod=bookings-api-7f4b8c9d6-8mvtr] heap 478MB / 512MBThere is the cause: an availability query for a bank-holiday weekend returned 8,412 results and the application loaded them all into memory.
The five typical causes of an OOMKilled and how to treat them:
| Cause | Signal | Fix |
|---|---|---|
| Limit too low | It always dies under normal load | Raise limits.memory |
| Memory leak | It dies periodically, faster each time | Fix the code; meanwhile, raising the limit delays the problem, it does not solve it |
| One-off spike (large query) | It only dies with certain requests | Paginate the query, cap the results |
| Runtime that cannot see the limit | The JVM or Node allocate based on the node's RAM | -XX:MaxRAMPercentage or --max-old-space-size with resourceFieldRef (03-03) |
| Wrong container | The one dying is the sidecar, not the app | Look at the container name in describe |
The fourth deserves a note, because it is the most frequent and the most invisible: a Java process without MaxRAMPercentage on a 32 GiB node with a limit of 512 MiB sizes its heap for the node's 32 GiB and dies as soon as it starts working. The previous lesson already gave you the tool to fix it.
ephemeral-storage and disk eviction
ephemeral-storage and disk evictionThere is a third resource that almost nobody declares and that causes baffling incidents: ephemeral storage, that is, the node disk the container uses for:
- The write layer of its file system (anything it writes outside a volume).
emptyDirvolumes.- The container logs (whatever it writes to stdout/stderr, which the kubelet stores on the node's disk).
It is declared just like the others:
resources:
requests:
cpu: 250m
memory: 256Mi
ephemeral-storage: 1Gi
limits:
cpu: "1"
memory: 512Mi
ephemeral-storage: 2GiWhat happens when the limit is exceeded: the kubelet evicts the pod, with Reason: Evicted:
kubectl get pods -n rutas-norte-pro | grep Evicted
kubectl describe pod notifications-worker-6f7d9-abcde -n rutas-norte-pro | grep -A3 "Status:"notifications-worker-6f7d9-abcde 0/1 Evicted 0 34m
Status: Failed
Reason: Evicted
Message: Pod ephemeral local storage usage exceeds the total limit of containers 2Gi.A relevant nuance: unlike an OOMKilled, disk eviction is not immediate. The kubelet checks usage roughly every 10 seconds, so a process can fill the disk before it gets evicted.
And the bigger problem: if the node's disk fills up, the kubelet goes into pressure and evicts pods even when their limits are in order. It is a failure that affects the whole node. The signals:
Conditions:
Type Status Reason Message
---- ------ ------ -------
MemoryPressure False KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False KubeletHasSufficientPID kubelet has sufficient PID available
Ready True KubeletReady kubelet is posting ready statusDiskPressure: True explains apparently random evictions. The order in which the kubelet picks its victims depends on the QoS class, which is the subject of the next lesson.
In Rutas Norte, the natural candidate to fill the disk is notifications-worker: if it produces one log line per email sent and tens of thousands are sent over a bank-holiday weekend, without log rotation the node's disk fills up. The practical rule: declare ephemeral-storage on any component that writes bulky logs or uses emptyDir, and keep an eye on DiskPressure on the nodes.
- Cluster overcommitment
Let us go back to that earlier line:
The sum of the CPU limits is 130% of the node's allocatable. Is the cluster misconfigured? No: that is normal and even desirable.
flowchart TB
subgraph N["Node: 4 allocatable cores"]
R["Committed REQUESTS: 2350m<br/>(guaranteed, the scheduler never exceeds them)"]
L["Summed LIMITS: 5200m<br/>(the scheduler does NOT look at them)"]
end
R --> S["Safe: the scheduler never commits<br/>more requests than capacity"]
L --> P["Overcommitment: only a problem<br/>if they ALL spike at once"]
The logic of overcommitment: pods do not consume their maximum simultaneously. notifications-worker works in bursts, bookings-api peaks at specific hours, web-store serves static assets with little consumption. Allowing the sum of limits to exceed capacity takes advantage of those gaps.
Where the danger lies, depending on the resource:
| Resource | If they all ask at once |
|---|---|
| CPU | They are all throttled in proportion to their request. High latency, no crashes. Recoverable |
| Memory | The node runs out of memory. The kubelet evicts pods. If it happens very fast, the kernel's OOM killer kills processes. Real crashes |
The operational conclusion is asymmetric and worth engraving:
Overcommitting CPU is acceptable and normal. Overcommitting memory aggressively is dangerous.
For Rutas Norte, the recommended ratios:
| Resource | limit / request ratio |
Reason |
|---|---|---|
| CPU | Between 2 and 4 | Absorb bank-holiday peaks without wasting capacity |
| Memory | Between 1 and 1.5 | The tighter it is, the lower the risk of cascading evictions |
Memory of bookings-postgres |
Exactly 1 (equal) | A database must never be an eviction candidate |
That last case, with requests == limits, has a name and very concrete consequences for who dies first: it is the Guaranteed class, and it is the subject of the next lesson.
And a memory warning that surprises many people: Kubernetes disables swap by default (although since 1.30 there is configurable beta support). With no swap, the kernel has no buffer: when memory runs out, it runs out. It is one more reason not to overcommit it.
- ResourceQuota: what it limits and how to read it
Everything above is per container. The ResourceQuota acts at another level: it puts a ceiling on a whole namespace. It is the object that settles the last module 2 debt.
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-dev
namespace: rutas-norte-dev
spec:
hard:
# --- Compute ---
requests.cpu: "2" # sum of the CPU requests of every pod
requests.memory: 4Gi
limits.cpu: "4" # sum of the limits
limits.memory: 8Gi
requests.ephemeral-storage: 10Gi
# --- Object counts ---
pods: "20"
services: "10"
configmaps: "20"
secrets: "20"
persistentvolumeclaims: "5"
services.loadbalancers: "0" # creating LoadBalancers is banned in dev
services.nodeports: "0"
count/deployments.apps: "10"
count/jobs.batch: "10"
count/cronjobs.batch: "5"
# --- Storage ---
requests.storage: 20Gi # sum of what every PVC asks forThe three families of limits:
| Family | Examples | What it controls |
|---|---|---|
| Compute | requests.cpu, limits.memory |
That one environment does not consume all the capacity |
| Object counts | pods, services, secrets |
That nobody saturates etcd or the control plane |
| Storage | requests.storage, persistentvolumeclaims |
The cost of the disks (module 5) |
Points that define its behaviour:
- It is a namespaced object and only affects its namespace. There are no cluster-wide quotas.
- It is applied at creation time. A pod that would exceed the quota is rejected; those that already exist are untouched.
- There can be several ResourceQuotas in a namespace. All of them apply and all of them must be satisfied.
- Terminated pods do not count. Those in
SucceededorFailedare excluded from the tally. services.loadbalancers: "0"is a very useful cost-control tool: every cloud LoadBalancer costs money.
Reading a quota:
Name: quota-dev
Namespace: rutas-norte-dev
Resource Used Hard
-------- ---- ----
configmaps 6 20
count/cronjobs.batch 0 5
count/deployments.apps 5 10
limits.cpu 2300m 4
limits.memory 3200Mi 8Gi
persistentvolumeclaims 1 5
pods 9 20
requests.cpu 1150m 2
requests.memory 1600Mi 4Gi
secrets 4 20
services 4 10
services.loadbalancers 0 0Two columns: Used (what is consumed right now) and Hard (the ceiling). This namespace is at 57% of its CPU requests and 40% of its memory. It is the view that answers "how much headroom do I have left in development?".
When somebody tries to go over:
kubectl scale deploy/bookings-api --replicas=12 -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp | tail -3deployment.apps/bookings-api scaled
LAST SEEN TYPE REASON OBJECT MESSAGE
3s Warning FailedCreate replicaset/bookings-api-6d8f7c9b Error creating: pods
"bookings-api-6d8f7c9b-" is forbidden: exceeded quota: quota-dev, requested: requests.cpu=250m,
used: requests.cpu=1900m, limited: requests.cpu=2Pay attention to a detail that is very important for diagnosis: the kubectl scale command succeeded. The Deployment now says 12 replicas. What fails is the creation of the pods by the ReplicaSet, and that failure does not appear in kubectl get deploy other than as a READY 8/12 that lasts for ever.
kubectl get deploy bookings-api -n rutas-norte-dev
kubectl describe deploy bookings-api -n rutas-norte-dev | grep -A4 ConditionsNAME READY UP-TO-DATE AVAILABLE AGE
bookings-api 8/12 12 8 3h
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
ReplicaProgressing False ProgressDeadlineExceededFaced with a Deployment stuck at READY x/y, always look at the ReplicaSet events and the namespace quota. It is one of the most frequent and least obvious causes, and it connects directly with the diagnosis of stuck rollouts you saw in module 2.
Scopes
A quota can be applied to only a subset of pods:
apiVersion: v1
kind: ResourceQuota
metadata:
name: high-priority-quota
namespace: rutas-norte-pro
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["high"]Available scopes: Terminating and NotTerminating (depending on whether they have activeDeadlineSeconds), BestEffort and NotBestEffort (depending on the QoS class from the next lesson), and PriorityClass. They let you give more headroom to the critical than to the experimental within the same namespace.
- The effect that forces you to declare resources
Here is the most useful and at the same time most disconcerting behaviour of ResourceQuotas:
If a namespace has a quota limiting
requests.cpuorlimits.memory, then EVERY pod created in it must declare that resource. If it does not, it is rejected.
The logic is obvious as soon as you think about it: to tally the sum of the namespace's requests.cpu, Kubernetes needs every pod to declare requests.cpu. A pod that did not declare it would make the tally impossible.
Demonstration. With the dev quota active, we try to create a pod with no resources:
Error from server (Forbidden): pods "test-no-resources" is forbidden: failed quota: quota-dev:
must specify limits.cpu for: test-no-resources; limits.memory for: test-no-resources;
requests.cpu for: test-no-resources; requests.memory for: test-no-resourcesThis is, by some distance, the most valuable side effect of quotas: it turns the discipline of declaring resources into something mandatory at platform level, rather than a recommendation people forget.
But it has an immediate cost: it breaks everything convenient. An ephemeral pod for debugging, a quick kubectl run, a one-off Job... they all fail. And this is where the missing piece appears, which is the subject of the next lesson: a LimitRange in the namespace injects default values into containers that declare nothing, so the pod passes the quota validation without anyone writing resources by hand.
flowchart LR
A["Pod with no resources"] --> B{"Is there a LimitRange<br/>in the namespace?"}
B -->|"Yes"| C["default and defaultRequest<br/>are injected"]
B -->|"No"| D["The pod still has<br/>no resources"]
C --> E{"Is there a compute<br/>ResourceQuota?"}
D --> E
E -->|"Yes, and the pod declares"| F["Tallied. ACCEPTED"]
E -->|"Yes, and it does not declare"| G["REJECTED<br/>must specify requests.cpu"]
E -->|"No"| F
ResourceQuota and LimitRange are inseparable. Setting a quota without a LimitRange is a defensible decision (it forces everyone to think about their resources), but it turns day-to-day work into a constant nuisance. The combination of the two is the standard configuration of any serious namespace.
- The quotas for the three Rutas Norte environments
Now for real: we apply the quotas and the debt is settled. The criterion is that dev must not be able to do harm and pro must have headroom for bank-holiday and school-holiday peaks.
rutas-norte-dev
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-dev
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
environment: dev
spec:
hard:
requests.cpu: "2" # 2 cores committed at most
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "25"
services: "10"
services.loadbalancers: "0" # no paid load balancers in dev
services.nodeports: "2"
persistentvolumeclaims: "4"
requests.storage: 10Gi
configmaps: "30"
secrets: "30"
count/deployments.apps: "15"
count/cronjobs.batch: "5"rutas-norte-pre
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-pre
namespace: rutas-norte-pre
labels:
app.kubernetes.io/part-of: rutas-norte
environment: pre
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 12Gi
pods: "30"
services: "15"
services.loadbalancers: "1" # one, to test the real Ingress
persistentvolumeclaims: "6"
requests.storage: 50Gi
configmaps: "40"
secrets: "40"
count/deployments.apps: "20"rutas-norte-pro
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-pro
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
environment: pro
spec:
hard:
requests.cpu: "24" # headroom for the module 9 autoscaling
requests.memory: 48Gi
limits.cpu: "48"
limits.memory: 64Gi
pods: "150"
services: "25"
services.loadbalancers: "3"
persistentvolumeclaims: "12"
requests.storage: 500Gi
configmaps: "60"
secrets: "60"
count/deployments.apps: "30"Comparison and justification:
| Resource | dev |
pre |
pro |
Why |
|---|---|---|---|---|
requests.cpu |
2 | 4 | 24 | pro serves real traffic and must grow on bank holidays |
requests.memory |
4Gi | 8Gi | 48Gi | Same, plus the cache and the database |
pods |
25 | 30 | 150 | The HPA needs a high ceiling in pro |
services.loadbalancers |
0 | 1 | 3 | Each one costs money every month |
requests.storage |
10Gi | 50Gi | 500Gi | The booking data only grows in production |
CPU limits/requests ratio |
2× | 2× | 2× | Moderate and uniform overcommitment |
Memory limits/requests ratio |
2× | 1.5× | 1.33× | More conservative the more critical it is |
Applying and verifying:
kubectl apply -f k8s/environments/dev/resourcequota.yaml
kubectl apply -f k8s/environments/pre/resourcequota.yaml
kubectl apply -f k8s/environments/pro/resourcequota.yaml
kubectl get resourcequota -Aresourcequota/quota-dev created
resourcequota/quota-pre created
resourcequota/quota-pro created
NAMESPACE NAME AGE REQUEST LIMIT
rutas-norte-dev quota-dev 8s pods: 9/25, requests.cpu: 1150m/2, ... limits.cpu: 2300m/4, ...
rutas-norte-pre quota-pre 7s pods: 4/30, requests.cpu: 500m/4, ... limits.cpu: 1000m/8, ...
rutas-norte-pro quota-pro 6s pods: 14/150, requests.cpu: 4200m/24, ... limits.cpu: 9600m/48, ...The debt is settled. Now, if somebody scales bookings-api to 50 replicas in development by mistake, or if a restart loop keeps creating pods, the damage stays inside rutas-norte-dev: the pods simply will not be created and production never notices. Remember from module 2 that the namespace isolates neither the network nor DNS; the quota is one of the few things a namespace really does isolate, and that is why it is so valuable.
A final note on the total sum. If you add up the requests.cpu of the three quotas you get 30 cores, and your cluster may have fewer. That is intentional: the quota is a per-environment ceiling, not a reservation. Nobody guarantees you can reach all three ceilings at once; what it guarantees is that no single one can go past its own.
- How to choose sensible values
The numbers above do not come from intuition. This is the method.
Step 1: measure, do not guess. You need metrics-server, which we enabled as an addon in module 1:
NAME CPU(cores) MEMORY(bytes)
bookings-postgres-5d8f6b9c4-k2mnp 180m 1842Mi
bookings-api-7f4b8c9d6-2xkqp 310m 387Mi
bookings-api-7f4b8c9d6-8mvtr 285m 371Mi
redis-cache-6c9d8f7b5-vn4qx 22m 148Mi
notifications-worker-6f7d9c8b4-lm3pt 95m 212Mi
web-store-5b7c9f4d8-h7pnk 8m 14MiThe detail of this command and its limitations (it is a snapshot, not a history) is the subject of Metrics Server. To calibrate properly you need history with percentiles, which is what Prometheus gives you.
Step 2: apply the formulas.
| Resource | Formula | Reasoning |
|---|---|---|
requests.memory |
95th percentile of usage × 1.2 | Memory is not throttled: fall short and you die |
limits.memory |
requests.memory × 1.3 to 1.5 |
Headroom for peaks, without overcommitting |
requests.cpu |
50th percentile (median) of usage | CPU is compressible: the median is enough for the guarantee |
limits.cpu |
requests.cpu × 2 to 4 |
Absorb bursts without throttling |
Note the asymmetry: memory from the 95th percentile, CPU from the median. It is the direct application of section 5.
Step 3: work it out for bookings-api in production. With two weeks of history, including a bank-holiday weekend:
resources:
requests:
cpu: 300m # ~ the median
memory: 534Mi # 445Mi x 1.2 -> we round to 512Mi
limits:
cpu: "1" # ~ 3x the request, covers the 940m peak
memory: 768Mi # 512Mi x 1.5Step 4: iterate. The initial values are wrong by definition. Review them:
- After every significant change to the application.
- After every high season (in Rutas Norte, after each bank holiday and after the summer).
- Whenever throttling goes above 5% or any
OOMKilledappears. - Quarterly, to reclaim committed and unused capacity.
Four rules that save you grief:
- Start generous and come down. A limit that is too high costs capacity; one that is too low causes outages in production. On the first iteration, prefer the waste.
- Multiply by replicas before looking at the quota.
requests.cpu: 300m× 8 replicas = 2400m of theprobudget. - Stateful components are different.
bookings-postgresmust haverequests == limitsfor memory: that is theGuaranteedclass from the next lesson, and a database must never be the first candidate to die. - Document where the numbers come from. A comment in the YAML with the measurement date and the percentile used turns the manifest into something reviewable:
resources:
# Calibrated 2026-07-28 over 14 days (includes the July bank holiday)
# CPU: median 280m, p95 620m | Memory: p95 445Mi, peak 502Mi
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "1"
memory: 768MiCommon Mistakes and Tips
| Mistake | Symptom | Fix |
|---|---|---|
memory: 512m in lower case |
Immediate CrashLoopBackOff |
m is milli. Use 512Mi |
Confusing M with Mi |
Unexplained OOMKilled with 6.9% less memory |
Always use Mi/Gi |
Pod Pending with Insufficient memory |
No node has room for the requests |
Lower the requests, add a node or autoscale |
| Looking at actual usage expecting the scheduler to use it | "If the node is at 10%, why does it not fit?" | The scheduler only looks at requests |
Only limits without requests |
More is reserved than expected | Kubernetes copies the limit into the request |
No memory limit |
A single pod can bring down the node | Always declare limits.memory |
| Very tight CPU limit | High latency with no restarts or visible errors | Check cpu.stat: nr_throttled/nr_periods |
| JVM or Node unaware of its limit | OOMKilled with an apparently sufficient limit |
MaxRAMPercentage or resourceFieldRef (03-03) |
Not checking --previous in the logs |
"There is nothing in the logs" after a restart | kubectl logs --previous |
| Quota with no LimitRange | kubectl run always fails |
Add a LimitRange (03-05) |
Deployment stuck at READY 8/12 |
The quota rejects the new pods, with no visible error on the Deployment | kubectl get events and describe quota |
| Quota set without telling the team | Deployments failing with no explanation | Communicate it and leave initial headroom |
Not declaring ephemeral-storage |
Evicted pods and DiskPressure on the node |
Declare it on anything writing logs or using emptyDir |
Tips:
- Every manifest template must ship with
resources. Put it in your base template so nobody has to remember. - Check the quota before scaling.
kubectl describe quota -n <ns>before akubectl scale. - Watch out for unused
requests. It is the biggest invisible cost in a cluster: committed capacity nobody takes advantage of. - Add an alert at 80% of quota. So you find out before a deployment fails, not during.
ephemeral-storageis not optional in production. A node inDiskPressureevicts healthy pods.
Exercises
Exercise 1: Trigger and diagnose both failures
- Create in
rutas-norte-deva pod calledmemory-hogwithlimits.memory: 128Mithat runs a process trying to allocate 300 MiB. Use thepolinux/stressimage orbusyboxwith a file in/dev/shm. - Observe its state, identify the
Reasonand theExit Code, and explain what137means. - Create a pod called
cpu-hogwithlimits.cpu: 100mrunning an infinite loop. - Check with
kubectl tophow much CPU it actually consumes and read/sys/fs/cgroup/cpu.statto work out the throttling percentage. - Explain in a table why one died and the other did not, even though both exceeded their limit.
Exercise 2: Put a quota on the three environments
- Apply the three ResourceQuotas from section 12.
- Show the current consumption of each namespace with
kubectl describe. - Try to scale
bookings-apiinrutas-norte-devto a replica count that exceeds the quota. Which command fails and which succeeds? Find the exact error message. - Try to create a pod with
kubectl runwithout declaring resources inrutas-norte-dev. Copy the error and explain it. - Work out how many replicas of
bookings-api(withrequests: cpu 250m, memory 256Mi) fit at most inrutas-norte-devaccording to each of the four compute limits, and say which one governs.
Exercise 3: Calibrate notifications-worker
You have these two weeks of production measurements, including a bank-holiday weekend:
CPU: median 95m, p95 340m, peak 780m
Memory: median 212Mi, p95 268Mi, peak 295Mi
Disk: logs 400 MiB/day, attachments emptyDir up to 600 MiB- Work out the CPU and memory
requestsandlimitsby applying the formulas from section 13, and justify why memory uses the p95 and CPU uses the median. - Decide a value for
ephemeral-storageand reason it out. - Write the complete
resourcesblock, with the traceability comment. - With 3 replicas in
rutas-norte-pro, work out what percentage of the production quota this component consumes. - If the CPU peak is 780m and you set
limits.cpu: 700m, what will happen to the component during the bank holiday? Is that acceptable for a mail worker? Compare with the answer if it werebookings-api.
Solutions
Solution 1
apiVersion: v1
kind: Pod
metadata:
name: memory-hog
namespace: rutas-norte-dev
labels:
app: lab
environment: dev
spec:
restartPolicy: Never
containers:
- name: stress
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "300M", "--vm-hang", "1"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mikubectl apply -f memory-hog.yaml
sleep 15
kubectl get pod memory-hog -n rutas-norte-dev
kubectl describe pod memory-hog -n rutas-norte-dev | grep -A6 "Last State"pod/memory-hog created
NAME READY STATUS RESTARTS AGE
memory-hog 0/1 OOMKilled 0 15s
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Wed, 05 Aug 2026 23:41:02 +0200
Finished: Wed, 05 Aug 2026 23:41:04 +0200The 137 is 128 + 9. By Unix convention, a process terminated by a signal returns 128 + signal number, and signal 9 is SIGKILL. It is the unmistakable signature of a violent death: the kernel gave the process no chance to clean up. (Its relative, 143 = 128 + 15, is SIGTERM: graceful termination, the one from module 2.)
The CPU hog:
apiVersion: v1
kind: Pod
metadata:
name: cpu-hog
namespace: rutas-norte-dev
labels:
app: lab
environment: dev
spec:
containers:
- name: loop
image: busybox:1.36
command: ["sh", "-c", "while true; do :; done"]
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mikubectl apply -f cpu-hog.yaml
sleep 60
kubectl top pod cpu-hog -n rutas-norte-dev
kubectl exec cpu-hog -n rutas-norte-dev -- cat /sys/fs/cgroup/cpu.statpod/cpu-hog created
NAME CPU(cores) MEMORY(bytes)
cpu-hog 100m 1Mi
nr_periods 604
nr_throttled 601
throttled_usec 53420118The infinite loop would like to consume a whole core (1000m), but kubectl top shows exactly 100m: the limit is enforced to the millicore. And the throttling is 601/604 = 99.5%: practically every period has been cut short. Even so, the pod is Running with no restarts.
memory-hog |
cpu-hog |
|
|---|---|---|
| Exceeded its limit | Yes (300 MiB > 128 MiB) | Yes (it wants 1000m > 100m) |
| Resource | Incompressible | Compressible |
| What the kernel could do | Nothing: requested memory cannot be "given more slowly" | Give it less CPU time |
| Result | OOMKilled, code 137 |
Running, throttled at 99.5% |
| Business impact | Loss of work in progress | Slowness |
Solution 2
kubectl apply -f k8s/environments/dev/resourcequota.yaml \
-f k8s/environments/pre/resourcequota.yaml \
-f k8s/environments/pro/resourcequota.yaml
kubectl describe quota -n rutas-norte-devName: quota-dev
Namespace: rutas-norte-dev
Resource Used Hard
-------- ---- ----
count/deployments.apps 5 15
limits.cpu 2300m 4
limits.memory 3200Mi 8Gi
pods 9 25
requests.cpu 1150m 2
requests.memory 1600Mi 4Gi
services 4 10
services.loadbalancers 0 0kubectl scale deploy/bookings-api --replicas=12 -n rutas-norte-dev
kubectl get deploy bookings-api -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --field-selector reason=FailedCreate | tail -2deployment.apps/bookings-api scaled
NAME READY UP-TO-DATE AVAILABLE AGE
bookings-api 3/12 12 3 3h
LAST SEEN TYPE REASON OBJECT MESSAGE
5s Warning FailedCreate replicaset/bookings-api-6d8f7c9b Error creating: pods
"bookings-api-6d8f7c9b-" is forbidden: exceeded quota: quota-dev,
requested: requests.cpu=250m, used: requests.cpu=1900m, limited: requests.cpu=2kubectl scale succeeds and the pod creation fails. The Deployment sits at READY 3/12 indefinitely. That asymmetry is the key to the diagnosis: the error is never on the object you touched, but in the ReplicaSet events.
Error from server (Forbidden): pods "test" is forbidden: failed quota: quota-dev:
must specify limits.cpu for: test; limits.memory for: test;
requests.cpu for: test; requests.memory for: testThe quota controls requests.cpu, requests.memory, limits.cpu and limits.memory. To be able to tally those four sums it needs every pod to declare them, so it rejects the one that does not. The elegant fix, without forcing anyone to write them by hand, is the LimitRange from the next lesson.
The maximum replica calculation, with requests: cpu 250m, memory 256Mi and limits: cpu 500m, memory 512Mi:
| Quota limit | Ceiling | Consumption per replica | Maximum replicas |
|---|---|---|---|
requests.cpu |
2000m | 250m | 8 |
requests.memory |
4096Mi | 256Mi | 16 |
limits.cpu |
4000m | 500m | 8 |
limits.memory |
8192Mi | 512Mi | 16 |
pods |
25 | 1 | 25 |
requests.cpu governs (tied with limits.cpu): 8 replicas. The effective limit is always the most restrictive one, and here it is the CPU. And careful: those 8 replicas would be the whole namespace, so you have to subtract what web-store, bookings-postgres, redis-cache and notifications-worker already consume.
Solution 3
- Applying the formulas:
requests.memory = p95 x 1.2 = 268Mi x 1.2 = 321.6Mi -> we round to 320Mi
limits.memory = 320Mi x 1.5 = 480Mi -> we round to 512Mi (covers the 295Mi peak comfortably)
requests.cpu = median = 95m -> we round to 100m
limits.cpu = 100m x 4 = 400m -> we raise it to 800m to cover the 780m peakThe asymmetry between memory and CPU is exactly the one from section 5. Memory uses the p95 because falling short does not mean "running slowly", it means OOMKilled: the confirmation email being sent is lost and the customer never gets their ticket. CPU uses the median because falling short only means the worker takes longer to drain the queue; the generous limit lets it speed up and catch up over a bank holiday.
ephemeral-storage: 400 MiB/day of logs plus up to 600 MiB of attachmentsemptyDir. Assuming daily log rotation and a safety margin:
requests.ephemeral-storage = 400Mi (logs) + 600Mi (emptyDir) = 1Gi
limits.ephemeral-storage = 1Gi x 2 = 2GiThe factor of 2 covers the case where log rotation fails for a day or where a bank holiday generates twice as many attachments. Without this limit, a rotation failure would fill the node's disk and cause DiskPressure, evicting the healthy pods sharing the node too, bookings-postgres included.
- The complete block:
- name: worker
image: registry.rutasnorte.example/notifications-worker:1.8.0
resources:
# Calibrated 2026-08-01 over 14 days (includes the 15 August bank holiday)
# CPU: median 95m, p95 340m, peak 780m -> request=median, limit>peak
# Memory: median 212Mi, p95 268Mi, peak 295Mi -> request=p95 x1.2
# Disk: 400Mi/day of logs + 600Mi of attachments emptyDir
requests:
cpu: 100m
memory: 320Mi
ephemeral-storage: 1Gi
limits:
cpu: 800m
memory: 512Mi
ephemeral-storage: 2Gi- With 3 replicas in
rutas-norte-pro:
| Resource | Consumption (3 replicas) | pro quota |
Percentage |
|---|---|---|---|
requests.cpu |
300m | 24000m | 1.25% |
requests.memory |
960Mi | 49152Mi | 1.95% |
limits.cpu |
2400m | 48000m | 5.0% |
limits.memory |
1536Mi | 65536Mi | 2.3% |
pods |
3 | 150 | 2.0% |
A very modest consumption: there is plenty of headroom for bookings-api to scale during the peaks.
- With
limits.cpu: 700mand a peak of 780m, the worker is throttled during the bank holiday. It does not die: it processes more slowly. The concrete effect is that the notification queue grows and the confirmation emails are delayed, perhaps from 30 seconds to several minutes.
Is that acceptable? For notifications-worker, yes, with caveats. It is an asynchronous process: the customer already has their booking confirmed on screen when the email goes out. A delay of minutes is annoying but does not break the business, and the queue drains itself once the peak passes. You would need to watch that the delay does not grow without bound: if the arrival rate exceeds the processing rate, the queue never recovers and then you really do have an incident.
For bookings-api the answer would be no. It is synchronous: the customer is sitting in front of the screen waiting. Throttling of 20% turns an 80 ms response into a 400 ms one, web-store starts hitting its TIMEOUT_MS of 2000, and the customer sees errors at the very moment of the highest sales of the year. In a user-facing component, the CPU limit must cover the peak comfortably.
That difference — between what merely runs slower and what the customer suffers — is what you must keep in mind when calibrating each component.
Conclusion
You no longer write resources by eye. You understand the whole model: requests are what the scheduler reserves when picking a node and what determines relative CPU priority within the node; limits are the ceiling the kubelet enforces through cgroups at runtime. You know that the scheduler only looks at requests, never at actual usage, which explains why a node at 10% usage can reject a pod. And you have mastered the units, including the two traps that cause real incidents: the 6.9% difference between M and Mi, and the lower-case 512m that sets a limit of half a byte.
You are clear about the fundamental distinction: CPU is compressible and exceeding it only produces throttling — diagnosable with the nr_throttled/nr_periods ratio from cpu.stat, invisible in the pod state — whereas memory is incompressible and exceeding it produces OOMKilled with code 137, with its five typical causes and the reflex of looking at kubectl logs --previous. You know about ephemeral-storage and the disk eviction that can bring down healthy pods across a whole node, and you understand why overcommitting CPU is normal and overcommitting memory is dangerous.
And you have settled the last module 2 debt: the three Rutas Norte environments have a ResourceQuota. rutas-norte-dev cannot go past 2 committed cores and 4 GiB, nor create a single LoadBalancer; rutas-norte-pro has 24 cores and 48 GiB of headroom for bank-holiday and school-holiday peaks. You can read kubectl describe quota, recognise the exceeded-quota error, and diagnose the treacherous case where kubectl scale succeeds and the Deployment sits at READY 8/12 for ever. And you have a method for choosing values: measure with kubectl top, apply the p95 to memory and the median to CPU, document the date and the percentile in the manifest itself, and iterate after every high season.
One loose end remains, and it has come up twice in this lesson. Setting a compute quota forces every pod to declare resources, and that breaks any quick kubectl run and any third-party manifest. The missing piece is the LimitRange, which injects default values into containers that declare nothing. And there is another, even more interesting loose end: we know that when a node runs short of memory somebody dies, but not who. The answer is not random: it depends on a classification Kubernetes assigns to each pod according to how it declared its resources. The next lesson, LimitRanges and Quality of Service (QoS) Classes, covers both things: the default, min, max and maxLimitRequestRatio fields, their exact interaction with the ResourceQuota, and the Guaranteed, Burstable and BestEffort classes with their effect on the kernel's oom_score_adj and on the kubelet's eviction order. By the end you will know why bookings-postgres must be Guaranteed and you will be able to trigger an eviction in your own minikube to see with your own eyes which pod falls first.
Kubernetes Course
Module 1: Introduction to Kubernetes
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
