22.2 Built-in Admission Controllers: ResourceQuota, LimitRanger, NamespaceLifecycle
Right, let’s talk about the digital hall monitors of your Kubernetes cluster: built-in admission controllers. These are the bouncers at the club door, checking your Pod’s ID (resourcequota) to see if it’s on the list, making sure it’s not wearing sneakers (limitrange), and confirming it’s not trying to get into a VIP section that’s already closed for the night (namespacelifecycle). They’re the first, and most crucial, line of defense against you (or your CI/CD pipeline) doing something profoundly silly.
Unlike their more flexible cousins, the webhooks, these controllers are compiled directly into the kube-apiserver binary. You enable them with a flag on the API server (--enable-admission-plugins=), and they just work. No deploying a Pod, no managing a service. The upside is rock-solid reliability; the downside is they’re not terribly customizable. You get what the Kubernetes designers give you, for better or worse.
ResourceQuota: The Cluster Accountant
Think of ResourceQuota as the cluster’s overly meticulous accountant. Its entire job is to sit in a namespace and scream “NOPE” whenever you try to create a resource that would push the total sum of allocated CPU, Memory, Storage, or even object counts over a predefined limit.
Why is this brilliant? It prevents a single team or project from accidentally (or purposefully) consuming all the cluster’s resources and causing a noisy, multi-tenant meltdown. It’s namespace-scoped, so you need one Quota object per namespace you want to govern.
Here’s the catch, and it’s a big one: the Quota accountant must be able to calculate the cost of your Pod before it’s admitted. This means you absolutely must specify resource requests and limits in your Pod spec. If you don’t, the accountant has no idea what the cost is, so it rejects the Pod on principle. This trips up everyone.
# A quota for the 'staging' namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-alpha-quota
namespace: staging
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "10"
services: "5"
# You can even quota storage classes and PVC counts. It's detailed.
Now, try to deploy this Pod into the staging namespace:
# pod-without-limits.yaml
apiVersion: v1
kind: Pod
metadata:
name: resource-hog
namespace: staging
spec:
containers:
- name: app
image: nginx
# See the problem? No requests/limits!
You’ll get a brutally efficient rejection message: pods "resource-hog" is forbidden: failed quota: team-alpha-quota: must specify limits.cpu,limits.memory,requests.cpu,requests.memory. It’s not being rude; it’s just doing its job with zero social skills.
LimitRanger: The Defaulting Police
This is where LimitRanger, the other half of this dynamic duo, comes in. If ResourceQuota is the accountant who rejects vague expense reports, LimitRanger is the kind colleague who pre-fills the form with sensible defaults before it gets to accounting.
You create a LimitRange object in a namespace, and it does two things:
- Sets defaults: If a Pod is created without
requestsorlimits, the LimitRanger admission controller will mutatethe Pod spec to add them. - Sets constraints: It can enforce min/max values for resources, so you can prevent a Pod from requesting a laughably small 1m CPU or a ridiculously large 64Gi of memory.
# A limit range for the 'staging' namespace
apiVersion: v1
kind: LimitRange
metadata:
name: sensible-defaults
namespace: staging
spec:
limits:
- default: # Applied if 'limits' are omitted
cpu: "500m"
memory: "512Mi"
defaultRequest: # Applied if 'requests' are omitted
cpu: "250m"
memory: "256Mi"
max: # The absolute maximum allowed in this namespace
cpu: "2"
memory: "2Gi"
type: Container
With this LimitRange in place, our previous resource-hog Pod would get automatically patched with the default requests and limits before being evaluated by the ResourceQuota. The accountant now sees a valid, priced-out Pod and lets it through. It’s a beautiful, automated workflow that saves you from your own forgetfulness.
Best Practice: Always define a LimitRange in every namespace that has a ResourceQuota. It removes a whole class of frustrating deployment errors.
NamespaceLifecycle: The Grim Reaper
This one is deceptively simple but vitally important. It governs the lifecycle of a namespace itself. Its most crucial job is to prevent the creation of new objects in a namespace that is currently terminating or has already been deleted.
Think about what happens when you kubectl delete namespace my-namespace. Kubernetes doesn’t just vaporize everything. It gracefully terminates all the resources inside it. This takes time. The NamespaceLifecycle controller ensures that during this termination period, no one can waltz in and create a new Pod, which would be both pointless and confusing.
It also prevents the non-existent namespace problem. If you try to create a Pod in a namespace that hasn’t been created yet, this controller will reject it. This feels obvious, but without this check, the API would happily accept the Pod and then just… have it exist in a phantom void. This admission controller keeps reality intact.
The design choice here is rock solid. There’s no questionable part; it’s just essential plumbing. The pitfall is not realizing it’s there and being briefly confused when your API call to a dead namespace is rejected. But that’s a you problem, not a Kubernetes problem.