Episode 4 β€” Configuration and Resource Management

Your app runs and is reachable β€” but its settings are baked into the image and it has no resource guardrails. This episode fixes both: externalize configuration (ConfigMaps/Secrets) and govern compute (requests, limits, quotas). These are the two levers behind reliability and cost.

Learning objectives

By the end of this episode you can:

  • Externalize config with ConfigMaps and know when to reach for Secrets instead.
  • Set requests and limits for CPU and memory, and read the units correctly.
  • Explain the three QoS classes and predict which Pod gets evicted or OOM-killed first.
  • Protect a cluster with ResourceQuotas and keep apps available during maintenance with PodDisruptionBudgets.

Why it matters

Two of the most common production incidents are "config change required a rebuild" and "one Pod ate the node." ConfigMaps kill the first; requests/limits kill the second. And requests are what the scheduler reads β€” set them wrong and pods either won't schedule or will fight over resources.


1. ConfigMaps β€” decouple config from image

A ConfigMap stores non-confidential data as key-value pairs, letting you decouple environment-specific config from your container image. Same image, different config per environment = portability.

apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  player_initial_lives: "3"                # property-like key
  game.properties: |                       # file-like key
    enemy.types=aliens,monsters
    player.maximum-lives=5

Four ways a Pod can consume it β€” the two you'll use daily are env vars and mounted files:

    # (a) selected keys as env vars
    env:
    - name: PLAYER_INITIAL_LIVES
      valueFrom:
        configMapKeyRef: { name: game-demo, key: player_initial_lives }
    # (b) all keys as env vars
    envFrom:
    - configMapRef: { name: game-demo }
    # (c) keys as files in a mounted volume
    volumeMounts:
    - { name: config, mountPath: /config, readOnly: true }
  volumes:
  - name: config
    configMap: { name: game-demo }

Behaviors you must remember β€” they cause real surprises:

BehaviorConsequence
Mounted ConfigMaps update automatically (after kubelet sync delay)Live config reload possible…
…except subPath mounts, which never updateSilent stale config
Env-var ConfigMaps do NOT updateNeed a Pod restart to pick up changes
Pod and ConfigMap must be same namespaceNo cross-namespace refs
Max size 1 MiBNot for large blobs
immutable: trueBlocks edits, boosts API-server performance at scale; delete+recreate to change

⚠️ Critical distinction: ConfigMap provides NO secrecy β€” data is stored in plaintext (Secrets are only base64, also not encryption). For passwords, keys, and tokens, use a Secret, and ideally back it with Azure Key Vault via the Secrets Store CSI driver (Episode 8).

🧠 Mnemonic: ConfigMap for the knobs, Secret for the keys.


2. Requests and limits β€” the two numbers that run your cluster

Every container should declare, per resource (CPU, memory):

  • request β€” the amount guaranteed and reserved. The scheduler uses requests to pick a node (sum of requests ≀ node capacity).
  • limit β€” the hard ceiling the kubelet/kernel enforces at runtime.
    resources:
      requests:            # what the scheduler reserves
        cpu: "250m"        # 250 millicpu = 0.25 core
        memory: "64Mi"
      limits:              # the enforced ceiling
        cpu: "500m"
        memory: "128Mi"

Units β€” the two classic traps

  • CPU: 1 = one full core; 500m = half a core (m = millicpu). CPU over-limit β†’ throttling (slowed, not killed).
  • Memory: Mi/Gi are power-of-two (mebibytes); M/G are decimal. ⚠️ M β‰  m: 128M is megabytes, 128m is millibytes (a tiny fraction of a byte β€” almost never what you want). Memory over-limit β†’ OOMKilled (terminated).
guaranteed floorCPU over limitmemory over limitrequest(scheduler reserves this)actual usagethrottled(slowed down)OOMKilled(terminated)limit(hard ceiling)

πŸ’‘ Tip: the scheduler checks capacity vs requests, not live usage. A node with plenty of free RAM will still refuse a Pod if its request doesn't fit the unreserved portion β€” the classic FailedScheduling: Insufficient memory.

QoS classes β€” who dies first under pressure

Kubernetes derives a Quality-of-Service class from how you set requests/limits. It decides eviction order when a node runs short:

QoS classHow to get itEviction priority
GuaranteedEvery container sets requests == limits for CPU and memoryEvicted last (safest)
BurstableAt least one request set, but not fully equal to limitsMiddle
BestEffortNo requests or limits at allEvicted first (riskiest)

🧠 Mnemonic β€” "GBB, best to worst": Guaranteed survives, Burstable bends, BestEffort breaks.

⚠️ Common failure signatures: OOMKilled = memory limit too low (or a leak); FailedScheduling = requests too high for any node; CPU throttling = CPU limit too low for load. Diagnose with kubectl describe pod <name> (the State/Last State fields) and kubectl top pod.


3. Governing whole namespaces: quotas and disruption budgets

Requests/limits are per-container. Two cluster-operator objects govern the aggregate β€” these are AKS best practices.

ResourceQuota β€” a hard budget per namespace

Caps total CPU/memory/objects a namespace may consume. Kubernetes does not overcommit: once the cumulative request total hits the quota, further deployments fail.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-app-team
spec:
  hard:
    cpu: "10"
    memory: 20Gi
    pods: "10"

⚠️ Powerful side effect: once a quota exists, every Pod in that namespace must declare requests/limits or the deployment is rejected. Pair with a LimitRange to inject sensible defaults so teams aren't forced to annotate every manifest.

🧠 Best practice (AKS): plan quotas at the namespace level; reject pods with no requests/limits; monitor and adjust.

PodDisruptionBudget β€” stay available during maintenance

Disruptions come in two flavors: involuntary (hardware failure, node VM deletion β€” mitigate with multiple replicas + multiple nodes) and voluntary (cluster upgrades, node drains, template updates). A PodDisruptionBudget (PDB) constrains voluntary disruptions so a minimum number of Pods stays running β€” the scheduler won't drain a node until the budget is satisfied.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-pdb
spec:
  minAvailable: 3            # or maxUnavailable: 2  (or a percentage like "60%")
  selector:
    matchLabels:
      app: nginx-frontend

This is what makes node upgrades (Episode 10) non-disruptive: AKS drains a node, but the PDB forces it to wait until replacements are Ready elsewhere.


Key takeaways

  • ConfigMap = non-secret config, decoupled from the image; mounted maps auto-update (not subPath, not env-vars); use Secrets/Key Vault for confidential data.
  • request is reserved and drives scheduling; limit is the enforced ceiling. CPU over-limit β†’ throttle; memory over-limit β†’ OOMKilled.
  • Watch the units: m = millicpu / millibytes, Mi/Gi = binary, M β‰  m.
  • QoS: Guaranteed (req==limit) > Burstable > BestEffort β€” BestEffort is evicted first.
  • ResourceQuota caps a namespace (and forces requests/limits); PodDisruptionBudget keeps a minimum available during voluntary disruptions like upgrades.

Quick recall

  1. Q: Which value does the scheduler read to place a Pod β€” request or limit? A: The request.
  2. Q: A container exceeds its memory limit β€” throttled or killed? A: OOMKilled (memory is enforced by termination; CPU is throttled).
  3. Q: How do you make a Pod Guaranteed QoS? A: Set requests == limits for both CPU and memory on every container.
  4. Q: You edit a ConfigMap consumed as env vars β€” does the running Pod see it? A: No β€” env-var ConfigMaps need a Pod restart.
  5. Q: What happens to new deployments once a namespace's ResourceQuota is exhausted? A: They fail (no overcommit).
  6. Q: Which object keeps N Pods running during a node drain/upgrade? A: A PodDisruptionBudget.

Exercises

Exercise 4.1 β€” Externalize a setting. An app reads DATABASE_HOST from the environment. Write a ConfigMap and the Pod env snippet so the value is db-service in the cluster, without rebuilding the image. If you later change it, what must you do for the running Pod?

Worked solution
apiVersion: v1
kind: ConfigMap
metadata: { name: app-config }
data: { DATABASE_HOST: "db-service" }
---
# in the Pod/Deployment container spec:
    env:
    - name: DATABASE_HOST
      valueFrom:
        configMapKeyRef: { name: app-config, key: DATABASE_HOST }

Because it's consumed as an env var, changing the ConfigMap does not update the running Pod β€” you must restart it (e.g. kubectl rollout restart deployment/<name>).

Exercise 4.2 β€” Predict the failure. A container has requests: {memory: 2Gi} and no limit, running on nodes with 4Gi allocatable. Two such Pods land on one node; a third is Pending. Later one running Pod's usage spikes to 3.5Gi and the node hits memory pressure. Explain both events.

Worked solution
  • Third Pod Pending: two Pods reserve 2Gi + 2Gi = 4Gi of requests, filling the node's allocatable; the scheduler refuses the third for Insufficient memory (FailedScheduling) β€” based on requests, not live usage.
  • Spike to 3.5Gi: with no memory limit, the container can grow until the node is under pressure; the kubelet then evicts Pods (BestEffort/Burstable first). Set a memory limit and appropriate request to make behavior predictable.

Exercise 4.3 β€” Make an upgrade safe. A 5-replica web Deployment must keep at least 3 Pods serving during cluster upgrades. Write the PDB, and name the other setting that makes the rollout itself zero-downtime.

Worked solution
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: web-pdb }
spec:
  minAvailable: 3
  selector:
    matchLabels: { app: web }

The PDB governs node drains (voluntary disruptions during upgrades). For the application rollout, the Deployment's readinessProbe (plus maxUnavailable/maxSurge) is what keeps traffic flowing to healthy Pods only.

Coming up

Your workloads are configured and resource-bounded. Now make them elastic. In Episode 5 we scale in two dimensions: pods (Horizontal Pod Autoscaler, KEDA) and nodes (cluster autoscaler, node autoprovisioning) β€” and learn exactly which lever to pull for which bottleneck.