Ravindra BagaleCourses & study guides मराठी

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.
Pin the image. Put settings in a ConfigMap. Set requests. Set limits. Apply and check QoS. Watch use. Protect the namespace.
From YAML to a sized pod: requests, limits and QoS

How: step by step

  1. Pin the image. Use a version tag like nginx:1.27, never latest.
  2. Put settings in a ConfigMap. Create it from a file or YAML, then use envFrom.
  3. Set requests. Measure normal use, for example 100m CPU and 128Mi memory.
  4. Set limits. Memory limit about 1.5 to 2 times the request. CPU limit is optional.
  5. Apply and check QoS. kubectl get pod -o jsonpath='{.status.qosClass}'.
  6. Watch use. kubectl top pod needs metrics-server.
  7. 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)

  1. 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-system
    

    What 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
    
  2. Watch real use

    kubectl top pod -n shop
    

    What it does: Compare real use with your requests.

    NAME             CPU(cores)   MEMORY(bytes)
    menu             1m           3Mi
    qos-guaranteed   1m           3Mi
    
  3. 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%)
    
  4. 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 shop
    

    What 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             -
    
  5. 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"}}
    
  6. Change a ConfigMap value

    kubectl patch configmap menu-config -n shop -p '{"data":{"CITY":"Mumbai"}}'
    kubectl exec -n shop menu -- printenv CITY
    

    What it does: Env vars are read only at start. The running pod still shows Pune until it restarts.

    configmap/menu-config patched
    Pune
    
  7. Clean up

    kubectl delete pod -n shop memory-hog too-big qos-guaranteed qos-burstable qos-besteffort plain
    

    What 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

  1. Check real use with kubectl top pod over a day.
  2. Fix a leak if memory only grows.
  3. Raise the limit, for example from 128Mi to 256Mi.
  4. 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

  1. Run kubectl describe nodes and read Allocated resources.
  2. Lower the request to the real need.
  3. 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

  1. Run kubectl describe quota -n shop.
  2. Delete unused pods or ask the admin to raise the quota.
  3. 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

  1. Apply the ConfigMap first.
  2. Or mark it optional: configMapRef with optional: true.
  3. 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.

How:
  1. Apply both files.
Solution
kubectl apply -f menu-config.yaml -f menu-pod.yaml

Easy Task 2: Read env

Why: Check it worked.

What to do: Print CITY and POD_NAME inside the pod.

How:
  1. kubectl exec printenv.
Solution
kubectl exec -n shop menu -- printenv CITY POD_NAME

Easy Task 3: QoS

Why: Know eviction order.

What to do: Print the QoS of every pod in shop.

How:
  1. custom-columns.
Solution
kubectl get pods -n shop -o custom-columns=N:.metadata.name,Q:.status.qosClass

Medium Task 4: Guaranteed

Why: Critical apps.

What to do: Make the menu pod Guaranteed.

How:
  1. Set requests equal to limits.
  2. 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.

How:
  1. Apply oom.yaml.
  2. jsonpath on terminated.
Solution
kubectl apply -f oom.yaml

Medium Task 6: Fix OOM

Why: Right sizing.

What to do: Raise the limit to 300Mi so memory-hog runs.

How:
  1. Edit the limit.
  2. Recreate.
Solution
limits: {memory: 300Mi}

Medium Task 7: Pending

Why: Read scheduler messages.

What to do: Create too-big and read the event.

How:
  1. describe pod.
Solution
kubectl describe pod too-big -n shop

Medium Task 8: LimitRange

Why: Safe defaults.

What to do: Write limitrange.yaml for shop.

How:
  1. kind: LimitRange.
  2. 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.

How:
  1. kind: ResourceQuota.
  2. Create pods until forbidden.
Solution
kubectl create quota shop-quota -n shop --hard=limits.memory=1Gi

Homework: 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

  1. 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.

  2. 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?