attribute namespace spend and enforce resource budgets
Kubernetes namespace-level FinOps: ResourceQuotas, LimitRanges, and team cost attribution
14 min read
Cloud billing shows cluster totals, not which namespace burned budget on unused CPU requests. ResourceQuotas cap team consumption, LimitRanges shape pod profiles, and OpenCost or Kubecost close the attribution loop—with labels finance can reconcile.
Why shared clusters hide team-level waste
FinOps conversations often stop at cloud billing dashboards, reserved capacity, and spot strategy. A large share of waste still originates inside the cluster: teams over-request CPU and memory, nobody tracks per-namespace consumption, and shared estates become black holes where resources disappear without an owner. Without enforcement, one team dev namespace can reserve sixty percent of schedulable capacity while production namespaces fight for the remainder. Symptoms include OOMKilled critical services, leadership unable to map spend to products, developers copying four CPU and eight GiB from staging without validation, and no feedback loop tying requests to cost. AWS Cost Explorer, GCP Billing, and Azure Cost Management show cluster or node pool totals—they cannot tell you that payments-api-dev spent budget on CPU requests that never ran.
Three layers: quota, limits, and attribution
Governance inside Kubernetes stacks three controls. ResourceQuota caps aggregate consumption per namespace—CPU and memory requests and limits, PVC count, pod count, LoadBalancer Services, and deployment objects. LimitRange sets per-container defaults, maximums, and maxLimitRequestRatio so a single pod cannot destabilize scheduling with tiny requests and huge limits. Cost attribution labels namespaces with team, cost-center, environment, and product, then feeds OpenCost or Kubecost so showback dashboards map utilization back to owners. Quotas enforce boundaries; limits shape workload profiles; attribution closes the visibility loop finance needs.
ResourceQuota: namespace budgets for compute and objects
Place ResourceQuota in each tenant namespace. Separate requests.cpu and requests.memory—which drive scheduling—from limits.cpu and limits.memory—which cap runtime. Include object count quotas for pods, services, and deployments to prevent namespace sprawl from cluttering the API server and monitoring. Put cost-center labels on namespaces and quotas so billing exports join Kubernetes consumption to finance codes.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-payments-quota
namespace: payments-api
labels:
team: payments
cost-center: "cc-1042"
environment: production
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
persistentvolumeclaims: "10"
pods: "30"
services.loadbalancers: "2"
count/deployments.apps: "15"
requests.storage: 200GiLimitRange: defaults and bounds per container
Quotas cap the namespace total; LimitRange governs individual containers. default and defaultRequest inject CPU and memory when manifests omit resources—preventing unbounded containers and easing scheduling. max forces decomposition of oversized workloads. maxLimitRequestRatio blocks the anti-pattern of minimal requests with extreme limits that breaks bin-packing. Add a Pod-type max when init containers or sidecars stack resources.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: payments-api
spec:
limits:
- type: Container
default:
cpu: "0.5"
memory: 512Mi
defaultRequest:
cpu: "0.25"
memory: 256Mi
max:
cpu: "2"
memory: 4Gi
min:
cpu: "0.1"
memory: 64Mi
maxLimitRequestRatio:
cpu: "10"
memory: "4"
- type: Pod
max:
cpu: "4"
memory: 8GiCost attribution with OpenCost and PromQL estimates
Label every namespace consistently, deploy OpenCost or Kubecost per cluster, and scrape allocation metrics into Prometheus or your FinOps platform. Compare actual utilization cost against quota headroom to find over-allocated namespaces and teams bursting beyond sustainable levels. Adjust on-demand rates in PromQL to match your node pricing; OpenCost reconciles with cloud billing when CUR or billing export is configured.
scrape_configs:
- job_name: opencost
scrape_interval: 1m
static_configs:
- targets:
- opencost.opencost.svc.cluster.local:9003
metric_relabel_configs:
- source_labels: [__address__]
target_label: cluster
replacement: prod-us-east-1# CPU (example on-demand vCPU-hour rate)
sum by (namespace) (
rate(container_cpu_usage_seconds_total{
container!="", container!="POD"
}[24h])
) * 730 * 0.0312
# Memory (example GiB-hour rate)
sum by (namespace) (
container_memory_working_set_bytes{
container!="", container!="POD"
}
) / 1024 / 1024 / 1024 * 730 * 0.00388Progressive quotas across dev, staging, and production
Scale namespace budgets with environment risk: dev gets smaller CPU requests and tighter per-container max values; production receives higher quotas, more pods, LoadBalancer allowance, and ingress object caps. One team label and cost-center across all three namespaces keeps attribution consistent while limits differ.
apiVersion: v1
kind: Namespace
metadata:
name: payments-api-dev
labels:
team: payments
cost-center: "cc-1042"
environment: dev
managed-by: platform-team
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: payments-api-dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "15"
count/deployments.apps: "5"
requests.storage: 50Gi
---
apiVersion: v1
kind: LimitRange
metadata:
name: dev-defaults
namespace: payments-api-dev
spec:
limits:
- type: Container
default:
cpu: "0.25"
memory: 256Mi
defaultRequest:
cpu: "0.1"
memory: 128Mi
max:
cpu: "1"
memory: 2GiapiVersion: v1
kind: Namespace
metadata:
name: payments-api-production
labels:
team: payments
cost-center: "cc-1042"
environment: production
managed-by: platform-team
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: payments-api-production
spec:
hard:
requests.cpu: "16"
requests.memory: 32Gi
limits.cpu: "32"
limits.memory: 64Gi
pods: "50"
count/deployments.apps: "20"
count/ingresses.networking.k8s.io: "10"
services.loadbalancers: "3"
requests.storage: 500Gi
---
apiVersion: v1
kind: LimitRange
metadata:
name: production-defaults
namespace: payments-api-production
spec:
limits:
- type: Container
default:
cpu: "1"
memory: 1Gi
defaultRequest:
cpu: "0.5"
memory: 512Mi
max:
cpu: "4"
memory: 8Gi
maxLimitRequestRatio:
cpu: "8"
memory: "4"Provision namespaces and quotas with Terraform
Manual namespace creation drifts from policy. Encode team, environment, cost-center, and quota tiers in Terraform variables, review changes in pull requests, and apply through your existing pipeline. Nested for_each over teams and environments avoids the bug of reusing one team environment list for every tenant.
variable "teams" {
type = map(object({
cost_center = string
environments = list(string)
quota = map(object({
cpu_request = string
memory_request = string
cpu_limit = string
memory_limit = string
max_pods = string
}))
}))
}
locals {
team_envs = merge([
for team, cfg in var.teams : {
for env in cfg.environments :
"${team}-${env}" => {
team = team
environment = env
cost_center = cfg.cost_center
quota = cfg.quota[env]
}
}
]...)
}
resource "kubernetes_namespace" "team" {
for_each = local.team_envs
metadata {
name = "${each.value.team}-api-${each.value.environment}"
labels = {
team = each.value.team
cost-center = each.value.cost_center
environment = each.value.environment
managed-by = "terraform"
}
}
}
resource "kubernetes_resource_quota" "team" {
for_each = kubernetes_namespace.team
metadata {
name = "${each.value.metadata[0].labels.team}-quota"
namespace = each.value.metadata[0].name
}
spec {
hard = {
"requests.cpu" = local.team_envs[each.key].quota.cpu_request
"requests.memory" = local.team_envs[each.key].quota.memory_request
"limits.cpu" = local.team_envs[each.key].quota.cpu_limit
"limits.memory" = local.team_envs[each.key].quota.memory_limit
"pods" = local.team_envs[each.key].quota.max_pods
}
}
}Operational practices: defaults first, alerts, admission, and capacity mix
Deploy LimitRanges with sensible defaults before tight quotas; collect two to four weeks of usage, then tighten. Use PriorityClass so critical production pods schedule ahead of batch work when quotas are under pressure—not to bypass quotas, but to order preemption fairly. Route quota increase requests through ticketing for audit trail. Alert at eighty percent of ResourceQuota using kube-state-metrics. Enforce team, cost-center, and environment labels on Namespace creation with Kyverno or Gatekeeper. Pair quotas with cluster autoscaler or Karpenter so node count follows approved requests—configure expander and scale-down delays to limit waste. Reserve baseline capacity for steady-state quota usage; use spot for interruptible dev and CI workloads. Namespace FinOps is ongoing discipline: every CPU cycle and memory byte should have an owner, a budget line, and a business justification.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: production-critical
value: 1000000
globalDefault: false
description: "Critical production workloads"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-processing
value: 100
globalDefault: false
description: "Batch jobs and non-critical workloads"groups:
- name: namespace-quota
rules:
- alert: NamespaceQuotaNearLimit
expr: |
sum by (namespace, resource) (
kube_resourcequota{type="used"}
)
/
sum by (namespace, resource) (
kube_resourcequota{type="hard"}
) > 0.8
for: 15m
labels:
severity: warning
annotations:
summary: "Namespace {{ $labels.namespace }} above 80% of {{ $labels.resource }} quota"apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-namespace-cost-labels
spec:
validationFailureAction: Enforce
rules:
- name: required-cost-labels
match:
any:
- resources:
kinds: [Namespace]
validate:
message: "Namespaces must include team, cost-center, and environment labels."
pattern:
metadata:
labels:
team: "?*"
cost-center: "?*"
environment: "?*"Cluster-wide FinOps visibility and VPA right-sizing build on patterns from our FinOps guide for cloud-native platforms.
Enforce required namespace labels at admission with policies from our Policy as Code guide with Kyverno and Gatekeeper.
