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 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.
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.
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.
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.
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.
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.
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.
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.
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.
kubectl top reports actual usage, which aligns with requests, not limits. A pod with requests: 100m and limits: 500m may only use 100 m, but the cluster scheduler bases placement on requests. Cost‑allocation models that ignore this mismatch will over‑ or under‑charge workloads.127.0.0.1:10250/deviceplugins).cost-center being a valid UUID vs. free‑form text). Inconsistent values create split‑billing entries for what is logically the same service.container_cpu_usage_seconds_total with container!="" labels) increases API server pressure and Prometheus storage. Pin labels to namespaces or use record: rules to aggregate before storage.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy