Connecting Kubernetes to CostReveal, setup guide
The CostReveal team, , 9 minute read
Kubernetes is the odd one out among CostReveal's providers: there is no billing API. Your cloud provider bills you for the nodes; nothing bills you for the workloads running on them. The Kubernetes integration bridges that gap: it takes each node's cost from your cloud bill and divides it among the workloads that ran on that node by the CPU, memory and GPUs each requested or used, measured by an OpenTelemetry Collector you run in the cluster. This guide walks through setup step by step.
Kubernetes cost allocation is a different problem from cloud cost attribution. Cloud bills know what the nodes cost; they know nothing about the workloads on them. Without a bridge, teams either split the bill by guesswork or leave the cluster as one unattributed lump. The CostReveal integration is that bridge: measured resource usage from inside the cluster, priced by the actual billed cost from outside it. The result is workload-level cost with the same evidence labels as every other provider, and an idle figure that is usually the most actionable number on the page.
How it works
Two data sources combine:
- Your cloud bill, through your existing AWS, Azure, Google Cloud or Oracle Cloud connection. A billing tag on the cluster's nodes says which bill lines are the cluster's: on EKS it is
kubernetes.io/cluster/<name>set toowned; on GKE it is thegoog-k8s-cluster-namelabel. When you register the cluster, you give CostReveal that tag, and the cluster's cost pool for each day comes from the bill lines carrying it. - Usage metrics from the cluster itself, sent by an OpenTelemetry Collector running in the cluster to CostReveal's OTLP endpoint. The Collector uses the standard
k8s_clusterandkubeletstatsreceivers and reports CPU and memory requests, actual CPU and memory usage, and each node's allocatable capacity, along with every pod's namespace, workload, and labels. Itsk8s_objectsreceiver also reads each node's provider ID, the name the cloud bills the machine by, and the GPUs each node offers and each container requests.
Each node's own line on the bill is then divided among the workloads that ran on that node, by their measured shares of that node's CPU, memory and GPUs, so a core on a large node costs what that node costs, not the cluster's average, and a GPU node's GPUs, 80 percent of its cost unless you set otherwise, are paid for by the workloads that requested them. Lines that are no node's, such as disks, load balancers and the control plane, are divided across the whole cluster. What no workload requested or used is shown as idle, its own figure, never divided among workloads. Days with no metrics are shown as unmeasured, not divided at all.
What the Collector sends
The Collector authenticates with your project's ingest key and names the cluster in a header, so one endpoint serves every cluster. Each delivery is deduplicated by metric series and timestamp, so a Collector's retries never count twice. Samples older than two days, or for a day already summed, are refused rather than rewriting history.
Only the metrics CostReveal reads are kept: container CPU and memory requests and usage, and node allocatable CPU and memory. Pod metadata, namespace, workload kind and name, and labels, is recorded with them. Each Node and Pod object is cut inside the cluster before it is sent: a node to its name, labels, provider ID and GPUs, a pod to its UID, node and each container's name and GPUs, so nothing else of a pod's spec, such as its environment, leaves the cluster. Nothing about your applications beyond resource usage leaves the cluster.
Prerequisites
Before you start, you need:
- A cloud connection (AWS, Azure, Google Cloud or Oracle Cloud) covering the account the cluster's nodes are billed in. The cluster's cost comes from that bill.
- The billing tag that marks the cluster's nodes on the bill: for EKS,
kubernetes.io/cluster/<cluster-name>set toowned; for GKE, thegoog-k8s-cluster-namelabel set to the cluster name. - Helm, to install the OpenTelemetry Collector's official chart with the values CostReveal gives you, at the version CostReveal is certified against.
Step by step
1. Register the cluster. In CostReveal, register a new Kubernetes cluster for your project: its name, the billing tag key and value from the bill, the CPU weight percent, how much of a node's cost is its CPU's, the rest its memory's, which defaults to 50, and the GPU part, how much of a GPU node's cost is its GPUs', which defaults to 80.
2. Deploy the Collector. Install the OpenTelemetry Collector's chart twice with the values shown in the setup steps: one Collector reads the Kubernetes API, one on every node reads its kubelet. Both send to CostReveal's OTLP endpoint with your project's ingest key and the cluster header, and run with CPU and memory requests and a memory limit.
3. Wait for the first metrics. The cluster shows when its Collector last delivered. Once a whole day of metrics is summed, the Kubernetes page starts showing workload costs.
4. Review the division. Each workload's cost is split into its CPU, memory and GPU parts, and each cluster shows how much was priced by its nodes' own lines and how much was shared across it. Idle capacity and unmeasured days are shown as their own rows, never folded into workloads.
How the cluster lifecycle works
Register the cluster, deploy the Collector, and wait for the first full day of metrics. The cluster shows when its Collector last delivered, so a silent Collector is visible before it becomes a gap in the data. Once a whole day of metrics is summed, workload costs appear on the Kubernetes page.
From there the lifecycle is steady state: metrics arrive, days are summed, costs divide. If a later day delivers no metrics, that day is shown as unmeasured, its billed cost visible but undivided, rather than guessed. If the Collector is removed or the cluster is decommissioned, the history already summed stays; only new days stop.
Workload recognition in depth
Workloads are recognized from the standard Kubernetes kinds: Deployments, StatefulSets, DaemonSets, CronJobs, and Jobs. A pod belonging to none of these is shown as a standalone workload, so nothing vanishes from the page.
Every pod's namespace, workload name, and labels travel with the metrics, with the labels of its Deployment or other workload, its namespace and its node, which means attribution can follow the conventions you already have: namespaces per team, labels on Deployments per service, node pools per team. A pod's own label comes first, then its workload's, its namespace's and its node's. The division itself does not require labels; it needs the workload identity and the measured shares. Labels decide who owns the result. Unlabeled workloads are not a failure: they are shown as unknown, with namespace and workload name still visible, until labels give them an owner. Explore groups and filters by cluster, namespace and workload as it does by service or account, with idle capacity as its own row, and custom dimensions such as environment read the same labels.
How the sync works
Metrics arrive continuously through the OTLP endpoint and are summed per day. Each delivery is deduplicated by metric series and timestamp, so Collector retries never count twice. Samples older than two days, or for a day already summed, are refused rather than rewriting history.
For every closed month, the cloud bill's ledger for the cluster is checked against the attribution read back from the database on every dimension, exactly. The provider's own total is the second check where offered. A closed month with a difference is shown as a visible variance; an open month stays pending until the invoice closes.
How the division stays honest
The division follows one rule: each node's line is divided by that day's measured usage and allocatable capacity of that node, the cluster's shared lines by the whole cluster's, and the parts can never add up to more than the day billed. If the arithmetic would divide more than the pool, that is treated as a defect, not silently adjusted.
Workload shares carry the ALLOCATED basis: the cost is real, the division is measured, and the rule is inspectable. Idle capacity, unlabeled workloads, and days with no metrics stay unknown, shown plainly on the Kubernetes page.
Troubleshooting
- The cluster shows no cost. Check the billing tag: the key and value must match what the cloud bill actually carries for the cluster's nodes. EKS tags every node
kubernetes.io/cluster/<name>; GKE labels eachgoog-k8s-cluster-name. - The Collector's data is not arriving. Confirm the Collector is sending to the right OTLP endpoint with the project's ingest key and the cluster header naming this cluster.
- A day shows as unmeasured. The cluster delivered no metrics for that day, so its billed cost is shown, not divided. Check the Collector was running.
- Everything is shared, nothing priced per node. Each node is priced by its own line only when its provider ID matches a machine on the bill, and only when the Collector sends Node objects. Install the chart with the values shown, which send them; the Kubernetes page says when no node's line was found.
- Workloads are missing. Workloads are recognized from Deployments, StatefulSets, DaemonSets, CronJobs, and Jobs. A pod belonging to none is shown as its own workload.
Frequently asked questions
Does the Collector read our application data? No. It sends resource metrics: CPU and memory requests and usage, node capacity, and pod metadata (namespace, workload, labels), with each Node and Pod object cut inside the cluster to its name, labels, provider ID and GPU counts. Your application data, logs, environment, and traffic never leave the cluster.
Does a workload pay for what it requested or what it used? The greater of the two, for CPU and memory, over each day. Reserved capacity is paid for by the workload that reserved it, even if it sat unused, and a workload that used more than it requested pays for what it used. GPUs are counted by request, for as long as the container ran.
What is the CPU weight percent? How much of each node's cost counts as CPU cost, the rest as memory cost. It defaults to 50 and you can set it per cluster when you register it.
How are GPU nodes priced? On a node with GPUs, the GPUs take a part of its cost, 80 percent unless you set the cluster's GPU part otherwise, divided among the workloads by the GPUs each requested. GPUs no workload requested are idle. NVIDIA's and AMD's are both counted.
What about idle capacity? It gets its own figure, never divided among workloads. Idle is usually the most actionable number on the Kubernetes page: it is the cost of capacity nobody used.
Why is a day unmeasured instead of estimated? Because an estimate would be a guess with no evidence behind it. The billed cost for the day is still shown, so the total stays complete; it is simply not divided among workloads the Collector never saw.
What if our workloads have no labels? They are shown as unknown, with namespace and workload name still visible. Add labels for the owning team or service and the next summed day attributes them directly.
Does each cluster need its own Collector? Yes. Usage is measured inside the cluster, so each cluster needs its own Collector deployment. All clusters report to the same OTLP endpoint, distinguished by the cluster header.
Is our cluster data kept separate from other customers? Yes. Each organization is isolated by PostgreSQL row-level security, enforced in the database below the application layer.
What to do first
After connecting, look at idle first. Then check the unmeasured days: a Collector that stops reporting turns divisible cost into an undivided lump. Label your namespaces and workloads for their owning teams, and the workload costs become team costs with no further work.
Keep reading
- Connecting AWS to CostReveal: the Cost and Usage Report your cluster's cost comes from, including the tag activation most teams miss.
- Connecting Google Cloud to CostReveal: the BigQuery billing export behind your GKE cluster's cost.
- CostReveal vs CloudZero: how the two platforms divide Kubernetes spend differently.