Your cloud bill tells you what the cluster cost. It does not tell you which team caused it. OpenCost closes that gap: it takes the metrics you are already scraping, joins them to instance pricing, and gives you cost per namespace, per deployment, per label.
It is a CNCF project and, more usefully, a published specification. The allocation methodology is written down rather than being a vendor’s black box, which matters the first time someone in finance asks how a number was produced.
What it needs before you start
OpenCost is not a metrics pipeline. It queries one. You need Prometheus already scraping cAdvisor and kube-state-metrics, which any kube-prometheus-stack install gives you.
That dependency is the whole architecture. OpenCost reads container CPU and memory usage from your existing metrics, reads node pricing from the cloud provider’s API, multiplies, and writes the result back out as new metrics. If your Prometheus is missing kube-state-metrics, OpenCost reports zeros and says nothing about why.
Install against the Prometheus you have
The default chart values assume an in-cluster Prometheus at prometheus-server.prometheus-system. Point it at yours instead:
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm upgrade --install opencost opencost/opencost \
--namespace opencost --create-namespace \
--set opencost.prometheus.internal.serviceName=kube-prometheus-stack-prometheus \
--set opencost.prometheus.internal.namespaceName=monitoring \
--set opencost.prometheus.internal.port=9090 \
--set opencost.metrics.serviceMonitor.enabled=true
serviceMonitor.enabled=true is the part people forget. Without it OpenCost computes costs and exposes them on its own /metrics endpoint, and nothing scrapes them. The API works, the UI works, and every Grafana panel built on OpenCost metrics stays empty.
For Grafana Cloud or Amazon Managed Prometheus, use the external block instead:
opencost:
prometheus:
internal:
enabled: false
external:
enabled: true
url: https://prometheus-prod-01.grafana.net/api/prom
existingSecretName: opencost-prometheus-auth
Credentials go in a Secret with the keys DB_BASIC_AUTH_USERNAME and DB_BASIC_AUTH_PW, or DB_BEARER_TOKEN. Setting username and password inline in the values file puts them in the Helm release, which is stored in the cluster as a Secret nobody remembers to rotate.
What it actually emits
OpenCost writes a small set of metrics, and knowing their names is the difference between using it and clicking around the UI:
| Metric | What it carries |
|---|---|
node_total_hourly_cost |
Full hourly price of a node, by instance type |
node_cpu_hourly_cost, node_ram_hourly_cost |
The split OpenCost uses to price a pod |
kubecost_node_is_spot |
Whether the node is spot, which is where most of the variance lives |
container_cpu_allocation |
CPU cores allocated to a container over the window |
container_memory_allocation_bytes |
Bytes allocated to a container |
container_gpu_allocation |
GPUs allocated, if any |
pv_hourly_cost, pod_pvc_allocation |
Storage price and which pod is holding it |
kubecost_load_balancer_cost |
Per-service LB cost, usually a surprise |
kubecost_network_nat_gateway_egress_cost |
NAT gateway egress, usually a bigger surprise |
The kubecost_ prefix is a naming holdover from the project’s origins, not a sign you are using a commercial product.
Allocation is the key concept: OpenCost prices max(request, usage) for each container, not usage alone. A pod requesting 4 cores and using 0.2 is charged for 4, because that is what it took off the cluster. This is the correct behaviour and it is also the number that starts arguments, so say it out loud before you publish the first dashboard.
The allocation API
The UI is a convenience. The API is the product:
kubectl -n opencost port-forward svc/opencost 9003:9003
# Last 7 days, one row per namespace, summed
curl -sG 'http://localhost:9003/allocation/compute' \
--data-urlencode 'window=7d' \
--data-urlencode 'aggregate=namespace' \
--data-urlencode 'accumulate=true' | jq '.data'
Four parameters do nearly everything:
windowacceptstoday,week,7d,24h, or an RFC3339 range.aggregateacceptsnamespace,controller,pod,node,cluster, and (the useful one)label:team.stepsplits the window into buckets, sowindow=30d&step=1dgives you a daily series.accumulate=truecollapses the window into one number per key.
Aggregating by label is what turns this into showback. If every workload carries a team label, then aggregate=label:team is a chargeback report, and the gap between that total and the cluster total is your unallocated cost.
There are three more endpoints worth knowing: /assets for cluster infrastructure priced independently of workloads, /cloudCost for the non-Kubernetes spend once you wire up a cloud billing integration, and /inferenceCost/total for per-token model costs if you are running inference workloads.
Idle cost, and why the totals do not match
A cluster costs more than the sum of its pods. Nodes have unallocated capacity, system daemonsets take a slice, and control planes bill regardless. That difference is idle cost, and OpenCost reports it as its own line rather than smearing it across teams.
This is the first thing to explain to whoever consumes the report. If you distribute idle proportionally, high-request teams subsidise the cluster’s spare capacity. If you leave it separate, the platform team owns it, which is usually correct because the platform team is who decides node sizes and scaling policy. Either is defensible. Silently switching between them is not.
Sustained idle above roughly 30 percent is a capacity problem, not an accounting one: it means bin packing, requests, or the autoscaler need attention, and no amount of allocation reporting will fix it.
Retention is shorter than you think
The chart ships with:
opencost:
exporter:
retention1d: 15 # days of daily-resolution data
retention1h: 49 # hours of hourly-resolution data
Fifteen days. Anything longer lives in Prometheus, or does not exist. If you want month-over-month trends, either extend retention on your Prometheus and query the OpenCost metrics directly, or export the allocation API to a warehouse on a schedule. Discovering this at the end of the first quarter is a bad time to discover it.
On-prem and unusual hardware
Without a cloud pricing API there is nothing to multiply by, so supply the rates yourself:
opencost:
customPricing:
enabled: true
provider: custom
costModel:
CPU: 1.25 # per core per hour
RAM: 0.50 # per GiB per hour
GPU: 0.95
storage: 0.25
internetNetworkEgress: 0.12
Deriving those rates is the actual work: take the amortised cost of the hardware plus power, cooling, and the share of the datacentre contract, then divide by usable capacity rather than installed capacity. The numbers will be wrong. They will still be far more useful than no numbers, and they get better once someone disagrees with them in a review.
Alerts worth having
Cost dashboards get opened when someone is already worried. Alerts arrive earlier:
groups:
- name: cost
rules:
- alert: NamespaceCostSpike
expr: |
sum by (namespace) (
container_cpu_allocation * on(node) group_left() node_cpu_hourly_cost
)
>
2 * avg_over_time(
sum by (namespace) (
container_cpu_allocation * on(node) group_left() node_cpu_hourly_cost
)[7d:1h]
)
for: 6h
labels:
severity: warning
annotations:
summary: '{{ $labels.namespace }} is running at twice its weekly average cost'
Two more that repay the effort: a rule on kubecost_network_nat_gateway_egress_cost (NAT egress is the classic silent leak, and a chatty new service can double it overnight), and one on the ratio of spot to on-demand nodes, since a spot capacity shortage quietly refills your cluster with on-demand instances at three times the price.
Where it fits
OpenCost answers “what did this cost”. It does not answer “what should it cost”. There is no rightsizing engine, no forecast, no recommendation queue. Pair it with usage data from your own monitoring to find the workloads requesting far more than they use: the Prometheus and Grafana setup already has both halves.
The failure mode is not technical. It is publishing a per-team number that nobody agreed on the methodology for, then spending three meetings defending the arithmetic instead of reducing the bill. Agree on how idle is handled and whether you price requests or usage, write it in the dashboard description, and the numbers start doing their job.