Skip to content

Connect Kubernetes to Xplorr: OpenCost Cost Allocation for Any Cluster

Xplorr reads Kubernetes cost data from OpenCost, the open source Kubernetes cost monitor. Once a cluster is connected, Xplorr splits its cost by namespace, controller, pod and pod label, and reports idle capacity separately from workload cost.

Xplorr never talks to your Kubernetes API server and never holds cluster credentials. It only reads what OpenCost has already computed.

OpenCost must be running in the cluster before you connect it. Xplorr does not install it for you and does not duplicate its setup here.

Follow the OpenCost installation guide. A default install exposes the OpenCost API on port 9003, as the service opencost in the opencost namespace.

Check that the API answers from inside the cluster before going further:

Terminal window
kubectl -n opencost port-forward service/opencost 9003:9003

Then, in another terminal:

Terminal window
curl -sS "http://localhost:9003/allocation?window=1d&aggregate=namespace&step=1d"

A healthy OpenCost returns JSON with "code": 200 and a data array. If that call fails, fix OpenCost first. Everything below depends on it.

DimensionWhat you see
NamespaceTotal cost per namespace
ControllerCost per Deployment, StatefulSet, DaemonSet, Job and so on, with the controller kind shown beside it
PodCost per pod, with its namespace
LabelCost grouped by any pod label key OpenCost reported, for example team or app

Every row is broken into CPU, RAM, GPU, storage (persistent volumes), network (data transfer plus load balancers), shared cost and a total.

Idle capacity is reported separately. Provisioned node capacity that no workload requested is never spread across namespaces. It appears as its own row so you can see how much of the cluster bill nobody asked for.

Pick the one that matches your network.

MethodBest forWhat it needs
Push, recommendedPrivate clusters, and most clusters generallyOutbound HTTPS only. Nothing exposed to the internet.
PullClusters whose OpenCost endpoint Xplorr can already reachAn ingress or load balancer in front of OpenCost

Push mode is the default in the console, and the one that works for a cluster sitting behind a firewall or on a private network. Pull mode exists for clusters that already publish an OpenCost endpoint.

The ingest mode is fixed when the cluster is created. To change it, delete the cluster and add it again.

Both paths end up writing the same daily cost rows through the same upsert, so a day that arrives twice is replaced, never counted twice.

A CronJob runs inside your cluster. It reads OpenCost over cluster internal DNS and POSTs the response to Xplorr over outbound HTTPS, carrying a bearer token that can write cost data for that one cluster and nothing else.

Log in to console.xplorr.io and open Kubernetes from the left sidebar. Click Connect cluster. You need the admin role.

Fill in:

  • Cluster name (required, up to 120 characters), for example prod-eks
  • How cost data reaches Xplorr: choose Private cluster, which is selected by default and marked Recommended
  • Linked cloud account (optional): pick the cloud account that pays for the cluster nodes. See Link a cloud account below.
  • Accept cost data from the collector: leave checked

Click Connect. Xplorr opens the collector dialog straight away, because a push cluster does nothing until its collector is installed.

In the collector dialog, click Generate token.

The token appears once. It starts with xpk8s_ and is 32 bytes of random data behind that prefix.

Copy it now if you want it outside the manifest. Xplorr stores only a SHA-256 hash of it, so nobody, including Xplorr support, can show it to you again. If you lose it, generate a new one and revoke the old one.

What a token can do:

  • Write Kubernetes cost data for exactly one cluster in one organization
  • Nothing else. It cannot read cost data, cannot touch another cluster, and is not an API token.

The dialog shows a manifest with the token already injected. Use Copy or Download to save it as xplorr-collector.yaml, then apply it:

Terminal window
kubectl apply -f xplorr-collector.yaml

The manifest contains three objects:

ObjectNamePurpose
Namespacexplorr-collectorHolds the collector, separate from your workloads
Secretxplorr-ingest-tokenHolds the ingest token, read by the CronJob as an environment variable
CronJobxplorr-cost-collectorRuns hourly, reads OpenCost, POSTs the result to Xplorr

Defaults in the generated manifest:

SettingDefaultNotes
Schedule0 * * * * (hourly, UTC)concurrencyPolicy: Forbid, so runs never overlap
OpenCost URLhttp://opencost.opencost.svc.cluster.local:9003Set through the OPENCOST_URL environment variable
Window3dEach run re-sends the last three days, so a missed run heals itself
Imagecurlimages/curl, pinned to an exact tagA stock upstream image, not an Xplorr binary, so you can review what runs
Resources10m CPU and 32Mi memory requested, 200m and 128Mi limits

The collector posts to https://ingest.xplorr.io/api/v1/kubernetes/ingest/<clusterId>, already filled in as XPLORR_INGEST_URL in the generated manifest, sending the ingest token as a bearer header. That is the only outbound destination it needs, so an egress allowlist only has to permit that host over HTTPS.

If OpenCost does not run in the opencost namespace, edit OPENCOST_URL in the CronJob before applying.

The collector pod is deliberately unprivileged: no ServiceAccount token is mounted, the root filesystem is read only, every Linux capability is dropped, it runs as a non root user, and the only secret it holds is the ingest token.

Force a first run rather than waiting for the hour:

Terminal window
kubectl -n xplorr-collector create job xplorr-first-run --from=cronjob/xplorr-cost-collector
kubectl -n xplorr-collector logs job/xplorr-first-run

A successful run logs how many bytes it collected from OpenCost, then sent to Xplorr. The token is never printed.

In the console, the cluster’s status moves from Waiting for collector to Receiving, and Last data shows when the collector last posted.

The collector dialog lists every token for the cluster, showing its prefix and last four characters, who created it, and when it was last used. You can:

  • Generate token to issue another one, for example to rotate without downtime. Issue the new token, apply the updated manifest, then revoke the old one.
  • Revoke a token. Any collector still using it stops being able to send cost data immediately. Cost data already collected is kept, and the revoked token stays in the list with its last used timestamp as an audit trail.

Creating and revoking tokens are both recorded in the audit log. Only the display prefix is recorded, never the token.

Use this only when Xplorr can reach your OpenCost endpoint from outside the cluster.

Put an ingress or load balancer in front of the OpenCost API (port 9003 by default) and protect it. OpenCost has no authentication of its own, so an endpoint exposed without protection lets anyone read your cluster cost data.

Protect it with a header your ingress checks, for example a bearer token or a shared secret header.

Open Kubernetes in the console, click Connect cluster, and choose Reachable from Xplorr.

Fill in:

  • Cluster name
  • OpenCost API URL, for example https://opencost.example.com. It must be http or https. Credentials in the URL are rejected; put them in the auth header instead. Cloud instance metadata addresses are refused.
  • Auth header name (optional, defaults to Authorization). Letters, digits and hyphens only.
  • Auth header value (optional, up to 4096 characters). Stored encrypted and never shown again. Xplorr sends it on every call.
  • Linked cloud account (optional)
  • Include in the daily sync: leave checked

Click Test connection before saving. Xplorr calls your endpoint for a one day window and reports how many allocations came back.

Save the cluster, then click Sync now on its row. Xplorr fetches recent days and loads them. After that the cluster refreshes on the daily schedule.

The Test button on the cluster row re-runs the connection check at any time without loading data.

ModeCadenceWindow
PushWhatever the CronJob schedule says, hourly by defaultThe last 3 days on every run
PullA daily job, 03:30 UTC by defaultThe last 3 days, or 30 days on a cluster’s first ever sync

Re-sending a day replaces it rather than adding to it, so overlapping windows are safe.

A collector may heal a gap but not rewrite arbitrary history: the ingest endpoint accepts allocation days within the last 120 days and ignores anything older or dated in the future.

Linking a cluster to the cloud account that pays for its nodes turns on allocation coverage: Xplorr compares the cluster cost OpenCost reported against the billed compute and managed Kubernetes cost of that account over the same period.

Coverage near 100% means the cluster explains its node spend. A low number usually means OpenCost is missing part of the cluster, or that the account pays for a lot besides this cluster.

Linking also decides who sees the cluster. Members and viewers see clusters linked to cloud accounts they have been granted access to. A cluster with no linked account is visible to admins only.

The Kubernetes page shows, for the selected range (7, 30 or 90 days) and optionally a single cluster:

  • Cluster cost, Idle cost with idle as a percentage of cluster cost, number of Clusters, and Allocation coverage
  • Daily cost, a stacked chart of allocated workload cost against idle capacity per day
  • Cost breakdown, with tabs for Namespace, Controller, Pod and Label, showing CPU, RAM, GPU, Storage, Network, Shared, Total and each row’s share. Idle capacity sits in its own row at the bottom.
  • Allocation coverage by cluster, comparing each cluster’s cost with the compute bill of its linked account
  • Connected clusters, with source, linked account, status and last data

On the Label tab, pick a label key from the dropdown. Xplorr lists the label keys OpenCost actually reported for workloads in the range, so an empty dropdown means OpenCost sent no pod labels.

BadgeMeaning
ReceivingPush cluster, a collector has posted data
Waiting for collectorPush cluster with a token issued, nothing posted yet
No token issuedPush cluster with no active token. Open Collector and generate one.
SyncedPull cluster, last sync succeeded
Sync failedPull cluster, last sync failed. The error is shown on the page.
Not synced yetPull cluster that has never synced. Click Sync now.
DisabledThe cluster is off. Push clusters reject ingest, pull clusters are skipped by the daily job.
  • Connecting the cluster before installing OpenCost. There is nothing to read. Install OpenCost first and confirm its API answers.
  • Picking pull mode for a private cluster. If Xplorr cannot reach the endpoint, every sync fails. Use push mode; it is the default for a reason.
  • Leaving the OpenCost URL pointing at the wrong namespace. The generated manifest assumes OpenCost runs in the opencost namespace. Edit OPENCOST_URL if yours does not.
  • Losing the token and re-applying the old manifest. The token is shown once. Generate a new one, apply the new manifest, revoke the old token.
  • Exposing OpenCost without protection to make pull mode work. OpenCost has no authentication of its own. Always put an auth header in front of it.
  • Expecting a partial day. Xplorr stores whole UTC days. Today’s cost appears once the day is complete and the next run picks it up.

Status stays at “Waiting for collector”

The CronJob has not completed a successful run. Check it:

Terminal window
kubectl -n xplorr-collector get cronjob,job,pod
kubectl -n xplorr-collector logs job/<job-name>

Common causes: the CronJob was never created because the manifest was applied to the wrong cluster or context, egress to Xplorr is blocked, or OPENCOST_URL points somewhere OpenCost is not.

Invalid ingest token

The token in the Secret does not match any token for that cluster. Generate a new token, apply the refreshed manifest, and confirm the Secret really updated:

Terminal window
kubectl -n xplorr-collector get secret xplorr-ingest-token -o jsonpath='{.data.token}' | base64 -d | head -c 14

That prints only the recognisable prefix, which is enough to compare with the prefix shown in the console.

This ingest token has been revoked

Someone revoked it. Generate a new one and re-apply.

This cluster is configured for pull ingest

You applied a collector manifest to a cluster that was created in pull mode. Ingest mode cannot be changed after creation, so delete the cluster and add it again as a private cluster.

Payload too large or Too many allocation rows

The window is too wide for one request. The ingest endpoint caps the body size, the number of allocation sets per request and the number of rows. Reduce COST_WINDOW in the CronJob, for example from 3d to 1d, and let the hourly schedule keep the data current.

Too many ingest requests for this cluster

The collector is posting faster than the per cluster rate limit allows. An hourly schedule is far below it; a schedule of every minute is not.

OpenCost API not found

The URL does not serve the OpenCost allocation API. Xplorr tries /allocation and falls back to /allocation/compute for older releases. Give the base URL of the API, not a path below it.

OpenCost rejected the credentials

The auth header name or value is wrong for your ingress. Re-enter the value in the cluster dialog; a stored value is never shown again, so leaving it blank keeps the old one.

This address is not allowed

The URL points at a cloud instance metadata address. Those are refused deliberately. Use the real OpenCost endpoint.

Cluster cost looks far lower than the cloud bill

Check allocation coverage. OpenCost prices the nodes it can see, so a cluster spanning node groups OpenCost is not configured for, or an account paying for much more than this cluster, both show up as low coverage.

  • OpenCost installed and its API answering inside the cluster
  • Cluster added in Xplorr with the right ingest mode
  • Push mode: token generated and manifest applied with kubectl apply
  • Push mode: a collector run completed and the status shows Receiving
  • Pull mode: connection test passed and the first sync ran
  • Cloud account linked, if you want allocation coverage
  • Namespace costs visible on the Kubernetes page

Does Xplorr need access to my Kubernetes API? No. In push mode the collector reads only the OpenCost HTTP API and mounts no ServiceAccount token. In pull mode Xplorr calls the OpenCost endpoint you give it. Neither path uses cluster credentials.

Do I have to open a port for push mode? No. The collector makes an outbound HTTPS request. Nothing in your cluster needs to be reachable from the internet.

Can one token write to several clusters? No. A token is scoped to one cluster in one organization. A token for cluster A cannot write cluster B.

Can I run the collector on a different schedule? Yes. Edit the schedule field in the CronJob. Each run re-sends a rolling window, so a slower schedule still produces complete days, just later.

Does Xplorr support clusters outside AWS, Azure and GCP? Yes for push mode. The collector only needs OpenCost and outbound HTTPS, so on premises and other hosted clusters work. Allocation coverage needs a linked cloud account, so it stays empty for a cluster with no cloud bill behind it.

Who can connect or delete a cluster? Admins. Members and viewers can see clusters linked to cloud accounts they have access to, and read cost data, but cannot change the connection.

What happens to cost history if I delete a cluster? It is removed with the cluster. Export what you need first from Reports and Export.