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=5Four 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:
| Behavior | Consequence |
|---|---|
| Mounted ConfigMaps update automatically (after kubelet sync delay) | Live config reload possible⦠|
β¦except subPath mounts, which never update | Silent stale config |
| Env-var ConfigMaps do NOT update | Need a Pod restart to pick up changes |
| Pod and ConfigMap must be same namespace | No cross-namespace refs |
| Max size 1 MiB | Not for large blobs |
immutable: true | Blocks 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/Giare power-of-two (mebibytes);M/Gare decimal. β οΈMβm:128Mis megabytes,128mis millibytes (a tiny fraction of a byte β almost never what you want). Memory over-limit β OOMKilled (terminated).
π‘ 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 class | How to get it | Eviction priority |
|---|---|---|
| Guaranteed | Every container sets requests == limits for CPU and memory | Evicted last (safest) |
| Burstable | At least one request set, but not fully equal to limits | Middle |
| BestEffort | No requests or limits at all | Evicted 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-frontendThis 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. requestis reserved and drives scheduling;limitis 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
- Q: Which value does the scheduler read to place a Pod β request or limit? A: The request.
- Q: A container exceeds its memory limit β throttled or killed? A: OOMKilled (memory is enforced by termination; CPU is throttled).
- Q: How do you make a Pod Guaranteed QoS? A: Set requests == limits for both CPU and memory on every container.
- Q: You edit a ConfigMap consumed as env vars β does the running Pod see it? A: No β env-var ConfigMaps need a Pod restart.
- Q: What happens to new deployments once a namespace's ResourceQuota is exhausted? A: They fail (no overcommit).
- 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.