Chapter 59: Kubernetes Pods in Depth: Architecture, YAML Anatomy and Pod Types
59.3 Single-container pod: requests, limits, env, ConfigMap and QoS classes
Why: the problem
Suppose we run the Zomato menu service. One day, a memory leak makes one pod eat 6 GB. The node runs out of memory. Linux kills random processes, and three other teams' pods die too. Another day, a new pod stays Pending for an hour, because it asked for 8 CPUs on 2-CPU nodes. Both problems come from one place: requests and limits. They are two lines in the pod YAML, and most beginners skip them. In this lesson we build a single-container pod the right way: a pinned image, ports, env from a ConfigMap, requests, limits, and a QoS class. Then we break it on purpose with an out-of-memory kill and a CPU request that never fits.
What: every term in plain English
- Single-container pod — The most common pod: one app container (plus the hidden pause container).
- requests — The minimum CPU and memory the pod needs. The scheduler only places the pod on a node with this much free.
- limits — The maximum. Above the memory limit the container is killed (OOMKilled). Above the CPU limit it is slowed down (throttled).
- m (millicores) — CPU unit. 1000m = 1 CPU core. 250m = a quarter core.
- Mi / Gi — Memory units. 1Gi = 1024Mi. Use Mi and Gi, not M and G.
- OOMKilled — The kernel killed the container because it used more memory than its limit.
- QoS class — Guaranteed, Burstable or BestEffort. Kubernetes picks it from your requests and limits. It decides who is evicted first.
- ConfigMap — An object that holds plain settings, such as LOG_LEVEL=info, outside the image.
- envFrom — Loads every key of a ConfigMap as environment variables.
- Downward API — Lets the pod read its own name, namespace, node or limits as env vars.
- LimitRange — A namespace rule that gives default requests and limits to pods that have none.
How: step by step
- Pin the image. Use a version tag like nginx:1.27, never latest.
- Put settings in a ConfigMap. Create it from a file or YAML, then use envFrom.
- Set requests. Measure normal use, for example 100m CPU and 128Mi memory.
- Set limits. Memory limit about 1.5 to 2 times the request. CPU limit is optional.
- Apply and check QoS. kubectl get pod -o jsonpath='{.status.qosClass}'.
- Watch use. kubectl top pod needs metrics-server.
- Protect the namespace. Add a LimitRange and a ResourceQuota.
Example 1: A production-style single-container pod with a ConfigMap
Goal. A production-style single-container pod with a ConfigMap.
menu-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: menu-config
namespace: shop
data:
LOG_LEVEL: "info"
CITY: "Pune"
CACHE_SECONDS: "60"
menu-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: menu
namespace: shop
labels:
app: menu
spec:
containers:
- name: menu
image: nginx:1.27
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
envFrom:
- configMapRef:
name: menu-config
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
What each line does
kind: ConfigMap / data— Plain key-value settings. All values are strings, so quote numbers.kind: Pod— A bare pod is fine for learning. In real use a Deployment wraps it (chapter 18.7).imagePullPolicy: IfNotPresent— Pull only if the node does not have the image yet.envFrom.configMapRef— Every key in menu-config becomes an env var.valueFrom.fieldRef— Downward API: the pod learns its own name and node.requests.cpu: 100m— The scheduler reserves a tenth of a core.limits.memory: 128Mi— Hard ceiling. Above it the container is OOMKilled.limits.cpu: 500m— The container is throttled above half a core.
Green output
$ kubectl apply -f menu-config.yaml -f menu-pod.yaml
configmap/menu-config created
pod/menu created
$ kubectl exec -n shop menu -- printenv | grep -E 'LOG_LEVEL|CITY|POD_NAME|NODE_NAME'
LOG_LEVEL=info
CITY=Pune
POD_NAME=menu
NODE_NAME=ch59-worker
$ kubectl get pod menu -n shop -o jsonpath='{.status.qosClass}{"\n"}'
Burstable
Expected result. The pod reads settings from the ConfigMap and its own name from the Downward API. QoS is Burstable because requests are lower than limits.
Example 2: See the three QoS classes side by side
Goal. See the three QoS classes side by side.
qos.yaml
apiVersion: v1
kind: Pod
metadata:
name: qos-guaranteed
namespace: shop
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests: {cpu: 200m, memory: 128Mi}
limits: {cpu: 200m, memory: 128Mi}
---
apiVersion: v1
kind: Pod
metadata:
name: qos-burstable
namespace: shop
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {memory: 256Mi}
---
apiVersion: v1
kind: Pod
metadata:
name: qos-besteffort
namespace: shop
spec:
containers:
- name: app
image: nginx:1.27
What each line does
requests equal limits— Both CPU and memory equal: Guaranteed. Evicted last when the node is short of memory.requests lower than limits— Burstable. Evicted after BestEffort pods.no resources at all— BestEffort. First to be evicted. Never use it for real apps.{cpu: 200m, memory: 128Mi}— Flow style YAML. Same as writing the two keys on separate lines.
Green output
$ kubectl apply -f qos.yaml
$ kubectl get pods -n shop -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
NAME QOS
menu Burstable
qos-besteffort BestEffort
qos-burstable Burstable
qos-guaranteed Guaranteed
Expected result. You can predict the QoS class from the YAML alone.
Example 3: Break it on purpose: OOMKilled and a pod that never fits
Goal. Break it on purpose: OOMKilled and a pod that never fits.
oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: memory-hog
namespace: shop
spec:
restartPolicy: Never
containers:
- name: hog
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]
resources:
requests: {memory: 50Mi}
limits: {memory: 100Mi}
too-big.yaml
apiVersion: v1
kind: Pod
metadata:
name: too-big
namespace: shop
spec:
containers:
- name: app
image: nginx:1.27
resources:
requests: {cpu: "16", memory: 1Gi}
What each line does
stress --vm-bytes 250M— Tries to use 250 MB, but the limit is 100Mi.restartPolicy: Never— Stops the pod after the kill, so the reason stays visible.cpu: "16"— Asks for 16 whole cores. No kind node has that.
Green output
$ kubectl apply -f oom.yaml
$ kubectl get pod memory-hog -n shop
NAME READY STATUS RESTARTS AGE
memory-hog 0/1 OOMKilled 0 9s
$ kubectl get pod memory-hog -n shop -o jsonpath='{.status.containerStatuses[0].state.terminated.reason} {.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
OOMKilled 137
$ kubectl apply -f too-big.yaml
$ kubectl describe pod too-big -n shop | tail -3
Events:
Type Reason From Message
Warning FailedScheduling default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient cpu.
Expected result. Exit code 137 means killed by signal 9. Insufficient cpu means no node has enough free requested CPU.
Every command, step by step (kind lab with kubectl)
-
Install metrics-server on kind
printf '%s\n' \ 'apiVersion: v1' \ 'kind: ServiceAccount' \ 'metadata:' \ ' name: metrics-server' \ ' namespace: kube-system' \ '---' \ 'apiVersion: rbac.authorization.k8s.io/v1' \ 'kind: ClusterRole' \ 'metadata:' \ ' name: system:metrics-server' \ 'rules:' \ ' - apiGroups: [""]' \ ' resources: ["nodes/metrics"]' \ ' verbs: ["get"]' \ ' - apiGroups: [""]' \ ' resources: ["pods", "nodes"]' \ ' verbs: ["get", "list", "watch"]' \ '---' \ 'apiVersion: rbac.authorization.k8s.io/v1' \ 'kind: ClusterRoleBinding' \ 'metadata:' \ ' name: system:metrics-server' \ 'roleRef:' \ ' apiGroup: rbac.authorization.k8s.io' \ ' kind: ClusterRole' \ ' name: system:metrics-server' \ 'subjects:' \ ' - kind: ServiceAccount' \ ' name: metrics-server' \ ' namespace: kube-system' \ '---' \ 'apiVersion: rbac.authorization.k8s.io/v1' \ 'kind: RoleBinding' \ 'metadata:' \ ' name: metrics-server-auth-reader' \ ' namespace: kube-system' \ 'roleRef:' \ ' apiGroup: rbac.authorization.k8s.io' \ ' kind: Role' \ ' name: extension-apiserver-authentication-reader' \ 'subjects:' \ ' - kind: ServiceAccount' \ ' name: metrics-server' \ ' namespace: kube-system' \ '---' \ 'apiVersion: rbac.authorization.k8s.io/v1' \ 'kind: ClusterRoleBinding' \ 'metadata:' \ ' name: metrics-server:system:auth-delegator' \ 'roleRef:' \ ' apiGroup: rbac.authorization.k8s.io' \ ' kind: ClusterRole' \ ' name: system:auth-delegator' \ 'subjects:' \ ' - kind: ServiceAccount' \ ' name: metrics-server' \ ' namespace: kube-system' \ '---' \ 'apiVersion: v1' \ 'kind: Service' \ 'metadata:' \ ' name: metrics-server' \ ' namespace: kube-system' \ ' labels:' \ ' k8s-app: metrics-server' \ 'spec:' \ ' selector:' \ ' k8s-app: metrics-server' \ ' ports:' \ ' - name: https' \ ' port: 443' \ ' targetPort: https' \ '---' \ 'apiVersion: apps/v1' \ 'kind: Deployment' \ 'metadata:' \ ' name: metrics-server' \ ' namespace: kube-system' \ ' labels:' \ ' k8s-app: metrics-server' \ 'spec:' \ ' selector:' \ ' matchLabels:' \ ' k8s-app: metrics-server' \ ' template:' \ ' metadata:' \ ' labels:' \ ' k8s-app: metrics-server' \ ' spec:' \ ' serviceAccountName: metrics-server' \ ' containers:' \ ' - name: metrics-server' \ ' image: registry.k8s.io/metrics-server/metrics-server:v0.7.2' \ ' args:' \ ' - --cert-dir=/tmp' \ ' - --secure-port=10250' \ ' - --kubelet-preferred-address-types=InternalIP,Hostname' \ ' - --kubelet-use-node-status-port' \ ' - --metric-resolution=15s' \ ' - --kubelet-insecure-tls' \ ' ports:' \ ' - name: https' \ ' containerPort: 10250' \ ' readinessProbe:' \ ' httpGet:' \ ' path: /readyz' \ ' port: https' \ ' scheme: HTTPS' \ ' volumeMounts:' \ ' - name: tmp-dir' \ ' mountPath: /tmp' \ ' volumes:' \ ' - name: tmp-dir' \ ' emptyDir: {}' \ '---' \ 'apiVersion: apiregistration.k8s.io/v1' \ 'kind: APIService' \ 'metadata:' \ ' name: v1beta1.metrics.k8s.io' \ 'spec:' \ ' group: metrics.k8s.io' \ ' version: v1beta1' \ ' groupPriorityMinimum: 100' \ ' versionPriority: 100' \ ' insecureSkipTLSVerify: true' \ ' service:' \ ' name: metrics-server' \ ' namespace: kube-system' \ > metrics-server.yaml kubectl apply -f metrics-server.yaml kubectl rollout status deploy/metrics-server -n kube-systemWhat it does: The first command writes the whole file: a ServiceAccount, RBAC rules, a Service, the Deployment and the APIService that adds the metrics API. --kubelet-insecure-tls is needed on kind because kubelet certs are self-signed; never use it on a real cluster. Wait about 60 seconds before kubectl top works.
serviceaccount/metrics-server created deployment.apps/metrics-server created apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created deployment "metrics-server" successfully rolled out deployment.apps/metrics-server patched -
Watch real use
kubectl top pod -n shopWhat it does: Compare real use with your requests.
NAME CPU(cores) MEMORY(bytes) menu 1m 3Mi qos-guaranteed 1m 3Mi -
Check node allocation
kubectl describe node ch59-worker | sed -n '/Allocated resources/,/Events/p'What it does: Shows how much CPU and memory is already promised by requests.
Resource Requests Limits cpu 400m (5%) 700m (8%) memory 306Mi (3%) 512Mi (6%) -
Add a LimitRange
printf '%s\n' \ 'apiVersion: v1' \ 'kind: LimitRange' \ 'metadata:' \ ' name: defaults' \ ' namespace: shop' \ 'spec:' \ ' limits:' \ ' - type: Container' \ ' defaultRequest:' \ ' cpu: 100m' \ ' memory: 64Mi' \ ' default:' \ ' memory: 128Mi' \ ' max:' \ ' memory: 1Gi' \ > limitrange.yaml kubectl apply -f limitrange.yaml kubectl describe limitrange defaults -n shopWhat it does: The full file is written by the first command. defaultRequest is used when a container has no requests; default is used when it has no limits; max stops anyone asking for more than 1Gi.
limitrange/defaults created Type Resource Min Max Default Request Default Limit Container memory - 1Gi 64Mi 128Mi Container cpu - - 100m - -
Prove the default
kubectl run plain --image=nginx:1.27 -n shop kubectl get pod plain -n shop -o jsonpath='{.spec.containers[0].resources}'What it does: The pod had no resources, but the LimitRange filled them in.
{"limits":{"memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}} -
Change a ConfigMap value
kubectl patch configmap menu-config -n shop -p '{"data":{"CITY":"Mumbai"}}' kubectl exec -n shop menu -- printenv CITYWhat it does: Env vars are read only at start. The running pod still shows Pune until it restarts.
configmap/menu-config patched Pune -
Clean up
kubectl delete pod -n shop memory-hog too-big qos-guaranteed qos-burstable qos-besteffort plainWhat it does: Keep menu and menu-config for the next lessons.
pod "memory-hog" deleted pod "too-big" deleted
When it is red 1
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Cause. The app used more memory than limits.memory.
Fix, step by step
- Check real use with kubectl top pod over a day.
- Fix a leak if memory only grows.
- Raise the limit, for example from 128Mi to 256Mi.
- For Java, set -XX:MaxRAMPercentage=75 so the heap fits inside the limit.
When it is red 2
Warning FailedScheduling 0/3 nodes are available: 2 Insufficient memory.
Cause. The request is bigger than free memory on any node.
Fix, step by step
- Run kubectl describe nodes and read Allocated resources.
- Lower the request to the real need.
- Or add a bigger node, or let the cluster autoscaler add one (lesson 63.7).
When it is red 3
Error from server (Forbidden): pods "menu" is forbidden: exceeded quota: shop-quota, requested: limits.memory=128Mi, used: limits.memory=1Gi, limited: limits.memory=1Gi
Cause. A ResourceQuota in the namespace is full.
Fix, step by step
- Run kubectl describe quota -n shop.
- Delete unused pods or ask the admin to raise the quota.
- Apply again.
When it is red 4
Error: configmap "menu-config" not found
Cause. The pod starts before its ConfigMap exists, so it stays in CreateContainerConfigError.
Fix, step by step
- Apply the ConfigMap first.
- Or mark it optional: configMapRef with optional: true.
- Delete and recreate the pod.
Practice tasks
Easy Task 1: ConfigMap
Why: Settings outside the image.
What to do: Create menu-config and the menu pod.
- Apply both files.
Solution
kubectl apply -f menu-config.yaml -f menu-pod.yamlEasy Task 2: Read env
Why: Check it worked.
What to do: Print CITY and POD_NAME inside the pod.
- kubectl exec printenv.
Solution
kubectl exec -n shop menu -- printenv CITY POD_NAMEEasy Task 3: QoS
Why: Know eviction order.
What to do: Print the QoS of every pod in shop.
- custom-columns.
Solution
kubectl get pods -n shop -o custom-columns=N:.metadata.name,Q:.status.qosClassMedium Task 4: Guaranteed
Why: Critical apps.
What to do: Make the menu pod Guaranteed.
- Set requests equal to limits.
- Recreate the pod.
Solution
requests: {cpu: 200m, memory: 128Mi}
limits: {cpu: 200m, memory: 128Mi}Medium Task 5: OOM
Why: See the kill.
What to do: Run memory-hog and read the reason and exit code.
- Apply oom.yaml.
- jsonpath on terminated.
Solution
kubectl apply -f oom.yamlMedium Task 6: Fix OOM
Why: Right sizing.
What to do: Raise the limit to 300Mi so memory-hog runs.
- Edit the limit.
- Recreate.
Solution
limits: {memory: 300Mi}Medium Task 7: Pending
Why: Read scheduler messages.
What to do: Create too-big and read the event.
- describe pod.
Solution
kubectl describe pod too-big -n shopMedium Task 8: LimitRange
Why: Safe defaults.
What to do: Write limitrange.yaml for shop.
- kind: LimitRange.
- default and defaultRequest.
Solution
kind: LimitRange
spec:
limits:
- type: Container
default: {memory: 128Mi}
defaultRequest: {cpu: 100m, memory: 64Mi}Hard Task 9: Quota
Why: Team budgets.
What to do: Create a ResourceQuota of 1Gi memory limits and hit it.
- kind: ResourceQuota.
- Create pods until forbidden.
Solution
kubectl create quota shop-quota -n shop --hard=limits.memory=1GiHomework: try at home
Measure one of your own apps with kubectl top for 10 minutes, then write requests and limits for it and explain your numbers.
Recap
- Requests are for the scheduler; limits are the ceiling.
- Memory over limit means OOMKilled (137); CPU over limit means throttling.
- QoS is Guaranteed, Burstable or BestEffort, decided by requests and limits.
- Put settings in ConfigMaps; env vars change only after a restart.
- Use LimitRange and ResourceQuota to protect a namespace.
FAQ
Q. Should I set a CPU limit?
Many teams set only a CPU request, to avoid throttling. Always set a memory limit.
Q. Can I put secrets in a ConfigMap?
No. Use a Secret (lesson 61.6), and keep real values out of Git.
Interview questions
-
What is the difference between requests and limits?
Model answer: Requests are guaranteed amounts used for scheduling; limits are the maximum. Memory over limit is OOMKilled; CPU over limit is throttled.
-
Explain Kubernetes QoS classes.
Model answer: Guaranteed when requests equal limits for all containers, Burstable when some requests are set, BestEffort when none. Under memory pressure the kubelet evicts BestEffort first, Guaranteed last.
Quick quiz
Q1. What does the scheduler use: requests or limits?
Requests.
Q2. What happens above the memory limit?
The container is OOMKilled with exit code 137.
Q3. What happens above the CPU limit?
The container is throttled, not killed.
Q4. Requests equal limits for CPU and memory gives which QoS?
Guaranteed.
Q5. Does changing a ConfigMap update env vars in a running pod?
No. The pod must restart.
Samajla ka?