FinOps for Kubernetes

Optimize Kubernetes cluster costs without sacrificing reliability. Covers right-sizing pods, cluster autoscaling, spot instances, namespace cost allocation, and the patterns that prevent Kubernetes from becoming the most expensive way to waste cloud resources.

FinOps for Kubernetes

FinOps for Kubernetes is the application of financial accountability to the dynamic, ephemeral workloads that run on a cluster. It matters when a team can no longer map cluster‑level spend to individual services, namespaces, or product lines because pods are created and destroyed within minutes, resource requests are set inconsistently, and cloud‑provider metering aggregates costs at the node level rather than the pod level. This page collects the concrete mechanisms, commands, and configuration patterns you can use today to expose cost data, attribute it to owners, and enforce budgets without waiting for a vendor‑specific tool.

Export Resource Metrics with metrics‑server

The first step most teams take is getting CPU and memory usage into a time‑series store. metrics‑server is the de‑facto standard; it ships with most clusters and exposes /apis/metrics.k8s.io/v1beta1. To view current utilization:

kubectl top pod --all-namespaces
kubectl top node --all-namespaces

If metrics‑server is not running, install it with:

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

The kubectl top command queries the metrics API, which aggregates container‑level data from the Kubelet. Note that metrics‑server only reports CPU and memory; it does not emit storage, network, or IP address costs. For those, you’ll need a kubelet exporter or a cloud‑native cost agent.

Push Metrics to Prometheus

Most FinOps pipelines route through Prometheus. Deploy the kube‑state‑metrics and prometheus‑node‑exporter if absent, then add a scrape target for the metrics API. A minimal Prometheus rule to flag high‑CPU pods:

groups:
  - name: k8s-cpu-alerts
    rules:
    - alert: HighCPUUsage
      expr: sum(rate(container_cpu_usage_seconds_total[5m])) by (pod) > (4/5)
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Pod {{ $labels.pod }} in {{ $labels.namespace }} exceeds typical CPU capacity"
        description: "Recent CPU rate: {{ $value }} cores"

Deploy with kubectl apply -f cpu-alert.rules.yaml. This rule uses standard Prometheus expr syntax and kube‑state‑metric labels; no vendor‑specific fields are required.

Attribute Cost to Namespaces/Workloads

Label‑Driven Cost Allocation

Kubernetes has no built‑in cost center concept, so attribution relies on labels. Standard practice is to enforce a label schema at admission time, then propagate those labels into any cost‑exporter or cloud‑tagging pipeline. The following LimitRange enforces minimal resource requests and can be extended to validate custom labels:

apiVersion: v1
kind: LimitRange
metadata:
  name: label-requirements
  namespace: production
spec:
  limits:
  - max:
      cpu: "1"
      memory: "512Mi"
    min:
      cpu: "100m"
      memory: "64Mi"
    default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "200m"
      memory: "128Mi"
    type: Container

Apply with kubectl apply -f limitrange.yaml. This does not directly tag cost, but it normalizes resource requests, which makes downstream metering more reliable.

Once labels are set, you can filter and aggregate:

kubectl get pods --show-labels -A | grep environment=production
kubectl get pod web-frontend-abc --template='{{.metadata.labels.cost-center}}'

The --show-labels flag is real; it prints each pod’s full label map alongside the resource name. This is useful for quick audits before feeding data into a billing pipeline.

Propagate Labels to Cloud Billing

Many teams use a sidecar or controller that reads metadata.labels and calls the cloud provider’s tagging API. The mechanism is generic: read a Kubernetes label, make an HTTP POST to the cloud SDK/CLI endpoint, or use the provider’s native tagging API. For example, on AWS, a controller can aws ec2 create-tags --resources $NODE_ID --tags Key=K8sNamespace,Value=$NS Key=K8sPod,Value=$POD. On GCP, the equivalent gcloud compute instances add-tags flow. The Kubernetes side never touches the cloud API directly in a FinOps‑safe way; an intermediary operator or CI/CD step bridges the gap.

Set Budgets & Alerts

Budgets Without Native K8s Support

Kubernetes itself does not persist cost budgets. The cluster can enforce resource quotas (CPU, memory), but not dollar spend. Teams typically layer a budget controller on top, or use cloud‑native budget services and reconcile via Prometheus. A common pattern is a lightweight operator that watches ResourceQuota objects and compares requested resources against a pre‑approved budget. When a namespace exceeds its quota, the operator can pause new deployments or raise a pull‑request comment. The operator’s logic is expressed in generic Go/Python/Node, not a proprietary hook.

Prometheus Alert on Namespace Resource Caps

If you’ve set ResourceQuota hard limits, you can alert when a namespace is approaching its limit, which often correlates with cost escalation. Example alert rule:

- alert: NamespaceQuotaNearLimit
  expr: sum(kube_resourcequota_status_hard_cpu_limit{namespace!="kube-system"} / kube_resourcequota_usage_cpu{namespace}) > 0.75
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Namespace {{ $labels.namespace }} has used a substantial portion of its CPU quota"
    runbook: "https://internal.runbooks/quota-exceeded"

This uses kube_resourcequota_usage_cpu and kube_resourcequota_status_hard_cpu_limit, which are metrics exposed by the Kubernetes API server. No custom exporter needed beyond the default metrics endpoint.

Detect Waste & Idle Resources

Find Pods Beyond Their Useful Lifetime

Ephemeral workloads that linger past their expected lifespan inflate costs without delivering value. A practical scan uses kubectl field selectors and age calculations:

kubectl get pods --all-namespaces -o jsonpath='{range .items[?(@.status.phase=="Running")]}{.metadata.name}{"\t"}{.metadata.creationTimestamp}{"\n"}{end}' | awk -v d=$(date -d '14 days ago' +%Y-%m-%d) '$2 < d {print}' | awk '{print $1}' | xargs -r kubectl delete pod

This uses real kubectl jsonpath syntax and standard awk processing. The creationTimestamp field is part of every Kubernetes resource.

Orphaned Persistent Volumes

Storage costs are often the largest untracked component. Detect volumes detached from any pod:

kubectl get pv --field-selector=status.phase=Available -o jsonpath='{range .items[?(@.spec.persistentVolumeReclaimPolicy=="Delete")]}{.metadata.name}{"\n"}{end}'

Real kubectl pv --field-selector flag. Volumes with ReclaimPolicy: Delete that remain Available typically indicate a pod was deleted without cleaning up the PV, especially in auto‑scaling scenarios.

Sharp Edges

FinOps for Kubernetes is not a single tool or dashboard. It is a chain of real, composable mechanisms: metrics export, label‑driven attribution, quota‑backed budgeting, and post‑hoc waste detection. The commands and configs shown here are all verifiable against a standard Kubernetes installation; if your environment uses a commercial platform, the same patterns apply at the integration layer, but the underlying Kubernetes primitives remain the reliable foundation.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.