Two admission plugins, two jobs
The problem. Ten teams share one cluster. One team forgets resources on every pod; another deploys 200 replicas by mistake and eats every node. The platform team needs defaults for the forgetful and a budget for the greedy - per namespace.
What you need to know already: namespaces (15.26), requests/limits and QoS (17.1, 17.3), the path from kubectl apply to a running pod (15.11), ReplicaSets (15.16).
Two words first:
- admission - the checks the apiserver runs on every create/update after it knows who you are and that you may do it, and before it saves the object. An admission plugin (or admission controller) is one such check; it can change the object (fill in defaults) or reject it.
- LimitRange - a namespaced object with per-container defaults and min/max.
- ResourceQuota (short: quota) - a namespaced object with a total budget.
| LimitRange | ResourceQuota | |
|---|---|---|
| scope | each container / pod / PVC | the whole namespace |
| does | fills in defaults, enforces min/max per object | caps the sum (requests, limits, object counts) |
| when | admission, on create | admission on create, plus a controller keeping status.used |
| typical use | "every container gets 100m/128Mi unless it says otherwise" | "team-a may use at most 8 cores and 16Gi in total" |
A platform team usually installs both in every tenant namespace: the LimitRange so nobody ends up BestEffort by accident, the quota so no team can eat the cluster.
LimitRange
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: team-a
spec:
limits:
- type: Container
defaultRequest: # requests when the container sets none
cpu: 100m
memory: 128Mi
default: # limits when the container sets none
cpu: 500m
memory: 256Mi
max: # no container may set more than this
cpu: "1"
memory: 1Gi
min:
memory: 32Mi
Read the spec: type: Container = the rules apply to each container; defaultRequest / default = the requests / limits filled in when missing; max / min = the allowed range. Defaults are applied by the LimitRanger admission plugin at create time. The pod records it in an annotation (a free-text note on an object, 15.26):
# an illustration: team-a with the LimitRange and quota above (the quota mission builds it)
kubectl run bare --image=nginx:1.27 -n team-a
pod/bare created
kubectl get pod bare -n team-a -o jsonpath='{.metadata.annotations}'
{"kubernetes.io/limit-ranger":"LimitRanger plugin set: cpu, memory request for container bare; cpu, memory limit for container bare"}
kubectl get pod bare -n team-a -o jsonpath='{.spec.containers[0].resources}'
{"limits":{"cpu":"500m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}
The ordering rules worth knowing:
- A container with a limit but no request gets request = its own limit (not the defaultRequest) - that is the apiserver's normal defaulting, before admission.
- A container with a request but no limit gets the default limit - and if its request is higher than that default limit, the pod is rejected by validation (request > limit). Write your LimitRange defaults generously.
- Defaults do not touch existing pods. Create the LimitRange first, or roll the workloads.
min/max violations are admission errors on the pod:
$ kubectl run big --image=nginx:1.27 -n team-a --dry-run=client -o yaml > big.yaml # then set limits.cpu: 2
$ kubectl apply -f big.yaml
Error from server (Forbidden): error when creating "big.yaml": pods "big" is forbidden: maximum cpu usage per Container is 1, but limit is 2
# an illustration: team-a with the LimitRange and quota above (the quota mission builds it)
kubectl describe limitrange defaults -n team-a
Name: defaults
Namespace: team-a
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu - 1 100m 500m -
Container memory 32Mi 1Gi 128Mi 256Mi -
ResourceQuota
# an illustration: team-a with the LimitRange and quota above (the quota mission builds it)
kubectl create quota compute -n team-a --hard=requests.cpu=2,requests.memory=4Gi,limits.cpu=4,limits.memory=8Gi,pods=20
resourcequota/compute created
kubectl create quota NAME creates a ResourceQuota; --hard= lists the ceilings as key=value pairs: requests.cpu=2 = the requests of all pods together may add up to 2 cores, limits.memory=8Gi = all memory limits together at most 8Gi, pods=20 = at most 20 pods. kubectl get quota / describe quota show Used against Hard:
# an illustration: team-a with the LimitRange and quota above (the quota mission builds it)
kubectl get quota -n team-a
NAME AGE REQUEST LIMIT
compute 12m pods: 5/20, requests.cpu: 500m/2, requests.memory: 640Mi/4Gi limits.cpu: 2500m/4, limits.memory: 1280Mi/8Gi
kubectl describe quota compute -n team-a
Name: compute
Namespace: team-a
Resource Used Hard
-------- ---- ----
limits.cpu 2500m 4
limits.memory 1280Mi 8Gi
pods 5 20
requests.cpu 500m 2
requests.memory 640Mi 4Gi
Also countable: services, configmaps, secrets, persistentvolumeclaims, requests.storage, and any kind as count/deployments.apps, count/jobs.batch.
The side effect everyone trips on: once a quota covers requests.cpu (or limits.memory, etc.), every new pod must specify that resource, because the quota system cannot count what is not declared:
Error from server (Forbidden): pods "bare" is forbidden: failed quota: compute: must specify limits.cpu for: bare; limits.memory for: bare; requests.cpu for: bare; requests.memory for: bare
That is precisely why quota and LimitRange come as a pair: the LimitRange fills the values in, the quota then counts them.
Going over the total:
Error from server (Forbidden): pods "big" is forbidden: exceeded quota: compute, requested: requests.cpu=500m, used: requests.cpu=1800m, limited: requests.cpu=2
Where the error goes when a controller creates the pod
You never see those errors when a Deployment creates the pods - kubectl talked to the apiserver about the Deployment, which was fine. The ReplicaSet controller (the control-plane loop that creates pods for a ReplicaSet, 15.9) gets the error, and records it as an event on the ReplicaSet:
# an illustration: team-a with the LimitRange and quota above (the quota mission builds it)
kubectl get deploy many -n team-a
NAME READY UP-TO-DATE AVAILABLE AGE
many 4/6 4 4 2m
kubectl describe rs -n team-a -l app=many | tail -2
Warning FailedCreate 8s (x5 over 2m) replicaset-controller Error creating: pods "many-7c9f4d8b6c-" is forbidden: exceeded quota: compute, requested: pods=1,requests.cpu=250m, used: pods=4,requests.cpu=1, limited: pods=4,requests.cpu=1
(pods "many-7c9f4d8b6c-" - the name is still the generateName prefix: admission rejected it before a name was generated.) The same pattern applies to every admission rejection: Pod Security (lesson 17.39), quotas, a missing ServiceAccount (the identity a pod runs as, lesson 17.33). Deployment stuck below its replica count with no Pending pods = look at the ReplicaSet's events.
Quota and the cluster admin
A quota is namespaced and applies to everyone, cluster-admin (the all-powerful admin user) included. It is not RBAC (the permission system, lesson 17.30): RBAC says whether you may create a pod, quota says whether this one still fits. kubectl describe namespace team-a shows both the quotas and the limit ranges in one place - the first command when a team says "we cannot deploy".
What you can now do
- Give a namespace defaults (LimitRange) and a budget (ResourceQuota).
- Read the two quota errors ("must specify", "exceeded quota") and find them on the ReplicaSet when a Deployment is stuck below its replica count.