Hedronite · Ops Lesson · 01-Earth-DevOps · Tue 2026-10-06

AKS namespace guardrails ResourceQuota, LimitRange defaults, and a Rust census

The quota meters the namespace. The LimitRange fills in the pod. A ceiling with no default becomes the default.

Lesson Class: Ops (T2 · AKS + kube-rs census)
Cloud Referent: AKS managed namespaces · defaultResourceQuota · LimitRange
Automation: cargo script + kube 4.2.0 + Nix writeBashBin
Checked: cargo check exit 0 · census run on local k3s · terraform validate · AKS output illustrative
Paired Dev: typed Quantity and a quota fit check
Paired Cert: CKA ResourceQuota versus LimitRange
Meter, Filler, Ceiling
Quota sums the namespace. Defaults fill the container. Max bounds it.
Ceiling Becomes Default
A max with no default is copied into default and defaultRequest.
ARM owns the meter
A managed namespace carries the quota. The LimitRange stays yours.
A ceiling-only LimitRange is a default you did not choose.

<!-- hal:authoritative:yaml -->

The quota meters the namespace. The LimitRange fills in the pod. A ceiling with no default becomes the default.

§I. Frame

The last three K8s Ops ran GKE, EKS, GKE: Binary Authorization on 10-03, Fargate profiles on 09-30, surge upgrades on 09-27. AKS last appeared on 09-24 (Azure Disk CSI) and 09-12 (Pod Security Admission labels). Today returns to AKS.

The referent is the tenant namespace on a shared AKS cluster. Each tenant gets a ResourceQuota and a LimitRange. AKS managed namespaces put the quota under ARM. Nothing in that ARM object writes per-container defaults.

The problem for today: declare one managed namespace with a quota, add the LimitRange it lacks, then build a read-only kube-rs census that finds namespaces with no quota, no defaults, a ceiling standing in for a default, or usage within 10% of a hard limit.

§II. Meter, Filler, Ceiling

Meter, Filler, Ceiling (named technique). Name which object does each job before you read a rejection.

  1. Meter: ResourceQuota. It sums requests and limits across non-terminal pods in one namespace. If a quota lists cpu or memory keys, every new pod must state those values, or admission refuses it with 403 Forbidden. Existing pods are untouched when the quota changes.
  2. **Filler: LimitRange defaultRequest and default.** The LimitRanger admission plugin writes these into containers that omit them, at create time only. A pod created before the LimitRange keeps its old spec.
  3. **Ceiling: LimitRange max and min.** These bound each container and reject outliers.

Ceiling Becomes Default (named technique). If a Container LimitRange sets max and leaves default empty, the API server copies max into default, then copies default into defaultRequest. On the local k3s this fire, a LimitRange written with only max: {cpu: "1", memory: 1Gi} read back with all three equal. A Deployment with no resources then asked for a full CPU per pod and its ReplicaSet logged exceeded quota. A ceiling-only LimitRange is a default you did not choose.

The AKS owner. az aks namespace add and the ARM type managedClusters/managedNamespaces create the namespace with defaultResourceQuota (cpuRequest, cpuLimit, memoryRequest, memoryLimit), a default network policy, labels, an adoptionPolicy, and a deletePolicy. Microsoft's role table says Azure Kubernetes Service RBAC Admin cannot write resource quota or the namespace itself, so a tenant admin cannot raise their own meter. The docs list no LimitRange field. Add it yourself.

Two warnings from the same page. Adopting an existing namespace with a quota below current requests blocks new pods and scale-ups. deletePolicy: Delete removes every object in the namespace with the ARM resource.

§III. The declarations

The managed namespace in Terraform through azapi (terraform validate clean on 1.14.3 with azapi v2.13.0, not applied):

resource "azapi_resource" "tenant_payments" {
  type      = "Microsoft.ContainerService/managedClusters/managedNamespaces@2025-09-01"
  parent_id = var.cluster_id
  name      = "tenant-payments"
  location  = var.location

  body = {
    properties = {
      defaultResourceQuota = {
        cpuRequest    = "2000m"
        cpuLimit      = "4000m"
        memoryRequest = "4Gi"
        memoryLimit   = "8Gi"
      }
      defaultNetworkPolicy = { ingress = "AllowSameNamespace", egress = "AllowAll" }
      adoptionPolicy       = "Never"
      deletePolicy         = "Keep"
    }
  }
}

The LimitRange that every tenant needs, managed namespace or not (server dry-run accepted on local k3s v1.34.12):

apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-defaults
  namespace: tenant-payments
spec:
  limits:
  - type: Container
    defaultRequest: {cpu: 250m, memory: 256Mi}
    default: {cpu: 500m, memory: 512Mi}
    max: {cpu: "1", memory: 2Gi}

tenant-guardrails.yaml in the bundle also carries a plain ResourceQuota for clusters that do not use managed namespaces. aks-managed-namespace.tf holds the full module.

§IV. The Rust census

Automation is a cargo script with kube-rs. It lists Namespaces, ResourceQuotas, and LimitRanges once each with Api::all, groups the last two by namespace, and skips kube-* plus three add-on namespaces. Flags:

The quantity parser in the script is a few lines and handles the common suffixes. The paired Dev lesson builds the typed version.

Checked on the lab Mac. cargo check exit 0 (kube 4.2.0, k8s-openapi 0.28.0 feature v1_34). Real run against a throwaway local k3s, not AKS:

default	ATTENTION	no_quota,no_limitrange_defaults
tenant-a	ok
tenant-b	ATTENTION	no_quota,no_limitrange_defaults
tenant-c	ATTENTION	near_hard=compute-quota/requests.cpu:90%,default_is_max=ceiling-only/cpu,default_is_max=ceiling-only/memory
tenant-d	ATTENTION	no_limitrange_defaults
summary namespaces=5 attention=4

Illustrative AKS output (no AKS cluster or Azure credentials as of this fire):

tenant-payments	ok
tenant-legacy	ATTENTION	no_quota,no_limitrange_defaults
summary namespaces=2 attention=1

§V. How to run

cargo +nightly -Zscript ./aks-namespace-guardrail-census.rs

Point kubeconfig at the cluster first (az aks get-credentials for AKS). The script reads three lists and exits 2 when any namespace needs attention, so CI can gate on it. It never creates, patches, or deletes. Nix wrapper: aks-namespace-guardrail-census.nix (writeBashBin), built on the lab Mac this fire.

§VI. Close

Meter, Filler, Ceiling: the quota meters the namespace, LimitRange defaults fill each container at admission, and max bounds it. On AKS the meter can live in ARM through a managed namespace. The filler stays a Kubernetes object you own. The census reads all three and names the gap.

Paired Dev parses 500m and 1Gi into a typed Quantity and sums pod requests against the quota. Paired Cert walks the admission refusal and the LimitRange that admits the same pod.

Related