LimitRange Defaults and Pod Disruption Budgets
SummaryCovers LimitRange creation and behavior including default, defaultRequest,...
Covers LimitRange creation and behavior including default, defaultRequest,...
Covers LimitRange creation and behavior including default, defaultRequest, max, min, and maxLimitRequestRatio fields. Explains how LimitRanges interact with ResourceQuotas. Introduces Pod Disruption Budgets with minAvailable and maxUnavailable fields. Includes four exercises covering Chapters 16–18 material.
LimitRange Defaults and Pod Disruption Budgets
A ResourceQuota caps total namespace consumption. A LimitRange caps individual container consumption and — critically — provides default values for containers that don’t specify their own resources. Together, they form a complete resource governance story.
What Is a LimitRange?
A LimitRange is a namespaced admission controller that intercepts Pod creation requests and applies four types of constraints:
- default: Sets default resource limits for containers that don’t specify them.
- defaultRequest: Sets default resource requests for containers that don’t specify them.
- max: Sets the maximum allowed resource request/limit.
- min: Sets the minimum allowed resource request/limit.
When a Pod is created without resource specifications, the LimitRange mutates the Pod spec by injecting default values. When a Pod specifies values that violate min or max bounds, the API server rejects the request.
LimitRange YAML
apiVersion: v1
kind: LimitRange
metadata:
name: container-limits
namespace: dev
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 256Mi
defaultRequest:
cpu: "100m"
memory: 128Mi
max:
cpu: "2"
memory: 1Gi
min:
cpu: "50m"
memory: 64Mi
maxLimitRequestRatio:
cpu: "4"
memory: "4"
Field Breakdown
| Field | Purpose | Applied When |
|---|---|---|
default | Default limits injected into containers | Container has no resources.limits |
defaultRequest | Default requests injected into containers | Container has no resources.requests |
max | Maximum allowed value for both requests and limits | Always — rejects Pods exceeding this |
min | Minimum allowed value for both requests and limits | Always — rejects Pods below this |
maxLimitRequestRatio | Maximum ratio of limit/request | Always — prevents excessive overcommit |
How Defaults Are Applied
Consider a Pod created without resource specifications in a namespace with the above LimitRange:
apiVersion: v1
kind: Pod
metadata:
name: no-resources
namespace: dev
spec:
containers:
- name: app
image: nginx:1.25
The LimitRange admission controller mutates this Pod before it is persisted. After mutation:
kubectl get pod no-resources -n dev -o yaml | grep -A 10 resources
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
The container received default values for both requests and limits. This is the mechanism that prevents the enforcement rule problem described in the previous section: with a LimitRange providing defaults, Pods don’t need to explicitly specify resources to satisfy a ResourceQuota.
Max and Min Enforcement
If a Pod specifies resources outside the bounds:
kubectl run big-pod --image=nginx \
--requests='cpu=3' --limits='cpu=5' \
-n dev
Error from server (Forbidden): pods "big-pod" is forbidden:
[maximum cpu usage per Container is 2, but limit is 5,
maximum cpu usage per Container is 2, but request is 3]
Both the request (3) and limit (5) exceed the max (2). The API server rejects the Pod before it is scheduled.
Similarly, a Pod requesting below the minimum:
kubectl run tiny-pod --image=nginx \
--requests='cpu=10m' --limits='cpu=50m' \
-n dev
Error from server (Forbidden): pods "tiny-pod" is forbidden:
minimum cpu usage per Container is 50m, but request is 10m
maxLimitRequestRatio
This field constrains the overcommit ratio between limits and requests:
maxLimitRequestRatio:
cpu: "4"
With a ratio of 4, a container requesting 100m CPU can set a limit of at most 400m. A container requesting 100m with a limit of 500m is rejected:
Error from server (Forbidden): pods "overcommit" is forbidden:
cpu max limit to request ratio per Container is 4, but provided ratio is 5.000000
This prevents scenarios where a container requests minimal resources (getting scheduled easily) but bursts to unreasonable limits, potentially destabilizing the node.
LimitRange Types
The type field in a LimitRange can target different resource consumers:
| Type | Applies To |
|---|---|
Container | Individual containers within a Pod |
Pod | Total resources across all containers in a Pod |
PersistentVolumeClaim | Storage requests for PVCs |
A LimitRange can include multiple entries:
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 256Mi
max:
cpu: "2"
memory: 1Gi
- type: Pod
max:
cpu: "4"
memory: 2Gi
- type: PersistentVolumeClaim
max:
storage: 10Gi
min:
storage: 1Gi
The Container-level limits apply per container. The Pod-level limits apply to the sum across all containers. A Pod with four containers, each requesting 500m CPU, would hit a Pod-level max of 2 (4 × 500m = 2).
LimitRange and ResourceQuota Interaction
LimitRanges and ResourceQuotas work together:
- A ResourceQuota on
requests.cpuforces every Pod to specify CPU requests. - A LimitRange with
defaultRequest.cpuinjects CPU requests into Pods that don’t specify them. - Result: Pods without explicit resources get LimitRange defaults → defaults count against the ResourceQuota → no Pod is rejected for missing specifications.
The typical setup for a governed namespace:
# Create namespace
kubectl create namespace managed-ns
# Create LimitRange with defaults
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: managed-ns
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 256Mi
defaultRequest:
cpu: "100m"
memory: 128Mi
max:
cpu: "2"
memory: 1Gi
EOF
# Create ResourceQuota
kubectl create quota ns-quota \
--hard=requests.cpu=4,requests.memory=8Gi,limits.cpu=8,limits.memory=16Gi,pods=20 \
-n managed-ns
Now any Pod created in managed-ns either specifies resources (validated against LimitRange bounds) or receives defaults (validated against both LimitRange bounds and ResourceQuota totals).
Inspecting LimitRanges
kubectl describe limitrange container-limits -n dev
Name: container-limits
Namespace: dev
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 50m 2 100m 500m 4
Container memory 64Mi 1Gi 128Mi 256Mi 4
This tabular output gives a complete view of all constraints in a single glance.
Pod Disruption Budgets
A Pod Disruption Budget (PDB) protects application availability during voluntary disruptions — events initiated by a human or automation, not by hardware failure:
kubectl drain(evicts all Pods from a node)- Cluster autoscaler removing a node
- Rolling updates triggered by Deployment changes
- Manual
kubectl delete pod(when the PDB controller is enforcing)
A PDB ensures that a minimum number of Pods remain available (or equivalently, a maximum number are unavailable) at any point during the disruption.
minAvailable
Specifies the minimum number of Pods that must remain running:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: web
If the web application has 3 replicas and minAvailable: 2, Kubernetes allows at most 1 Pod to be evicted at a time. A kubectl drain on a node with one of these Pods succeeds. A drain attempting to evict 2 Pods from this set is blocked until one replacement Pod is ready.
minAvailable can also be a percentage:
spec:
minAvailable: "50%"
With 4 replicas and minAvailable: "50%", at least 2 Pods must remain available.
maxUnavailable
The inverse of minAvailable — specifies the maximum number of Pods that can be down simultaneously:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: web
With 3 replicas and maxUnavailable: 1, at most 1 Pod can be disrupted at a time. This is functionally equivalent to minAvailable: 2 with 3 replicas, but maxUnavailable scales more naturally — maxUnavailable: 1 means the same thing regardless of replica count.
Creating a PDB Imperatively
kubectl create pdb app-pdb \
--selector=app=web \
--min-available=2
Or with maxUnavailable:
kubectl create pdb app-pdb \
--selector=app=web \
--max-unavailable=1
Checking PDB Status
kubectl get pdb app-pdb
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
app-pdb 2 N/A 1 5m
ALLOWED DISRUPTIONS shows how many Pods can currently be evicted. If it shows 0, no voluntary disruptions are permitted — eviction requests are blocked.
PDB Constraints
- You can specify either
minAvailableormaxUnavailable, not both. - PDBs only protect against voluntary disruptions. An involuntary disruption (node crash, OOM kill) bypasses the PDB entirely.
- A PDB with
maxUnavailable: 0blocks all voluntary disruptions and should be used with extreme caution — it can prevent node drains and cluster upgrades. - PDBs require a selector that matches the Pods they protect. The selector must match the same labels used by the Deployment, StatefulSet, or ReplicaSet managing those Pods.
Exam Awareness
PDBs appear on the CKAD exam rarely and at a basic level. Know how to create one with kubectl create pdb and understand the difference between minAvailable and maxUnavailable. The conceptual understanding — “PDBs ensure N Pods survive voluntary disruptions” — is sufficient.
Exercises
Work through these exercises on your Kind cluster. Complete YAML manifests and step-by-step solutions are provided in Chapter 19.
Exercise 1: ConfigMap as Environment Variables and Volume Mount
Create a file named app.properties with the following content:
db.host=postgres.prod.svc
db.port=5432
cache.ttl=300
Create a ConfigMap named app-settings from this file. Create a Deployment named config-app with 2 replicas running busybox (command: sleep 3600) that:
- Mounts the ConfigMap as a volume at
/etc/config - Also injects
db.hostfrom the ConfigMap as an environment variable namedDATABASE_HOSTusingvalueFrom
Verify that the file exists at /etc/config/app.properties and that the DATABASE_HOST environment variable is set. Then update the ConfigMap and verify the volume-mounted file updates while the environment variable does not.
Exercise 2: TLS Secret with Nginx
Generate a self-signed TLS certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=myapp.example.com"
Create a TLS Secret named nginx-tls from the certificate and key. Create an nginx Pod named secure-nginx that mounts the TLS Secret at /etc/nginx/ssl with readOnly: true. Verify the certificate and key files exist inside the container.
Exercise 3: Locked-Down Security Context
Create a Pod named secure-pod running busybox (command: sleep 3600) with the following security constraints:
- Run as user 1000, group 1000
- Read-only root filesystem
- No privilege escalation
- Drop all Linux capabilities
- Mount an
emptyDirvolume at/tmpfor writable space
Verify:
idshows UID 1000, GID 1000touch /testfails (read-only filesystem)touch /tmp/testsucceeds (writable emptyDir)
Exercise 4: ServiceAccount with RBAC
Create a namespace named rbac-lab. In that namespace:
- Create a ServiceAccount named
pod-inspector - Create a Role named
pod-readergrantinggetandlistonpods - Create a RoleBinding named
pod-reader-bindingbinding the Role to the ServiceAccount - Create a Pod named
inspectorusing thepod-inspectorServiceAccount, runningcurlimages/curlwith commandsleep 3600
Verify:
kubectl auth can-i get pods --as=system:serviceaccount:rbac-lab:pod-inspector -n rbac-labreturnsyeskubectl auth can-i delete pods --as=system:serviceaccount:rbac-lab:pod-inspector -n rbac-labreturnsnokubectl auth can-i get secrets --as=system:serviceaccount:rbac-lab:pod-inspector -n rbac-labreturnsno