Kubernetes Cluster Cost Allocation
Overview
Section titled “Overview”finfocus cost cluster allocates the cost of a Kubernetes
cluster’s nodes (and, on EKS, its control plane and each Fargate pod) across
the workloads running on them. With no window, that cost is the monthly run
rate. With --from and --to, it is the actual spend over that window.
A usage-source plugin reports cluster state, FinFocus prices the
reported nodes through its normal pricing plugins, and an allocator plugin
splits each node’s cost across the workloads. By default, unused
capacity is its own idle row. An allocation policy can set idle or
system_workloads to share, which folds that cost into the workloads on
the same node. Every run enforces a conservation invariant: allocated rows
must sum to the priced total.
Prerequisites
Section titled “Prerequisites”-
A reachable cluster via kubeconfig (
KUBECONFIG,~/.kube/config, or in-cluster config). Read access requires list on nodes, pods, ReplicaSets, and Jobs — see the minimal ClusterRole inplugins/kubernetes/deploy/clusterrole.yaml. -
The kubernetes plugin (usage source and allocator):
Terminal window finfocus plugin install kubernetes -
A pricing plugin for the nodes’ provider and region, for example aws-public pinned to the nodes’ region:
Terminal window finfocus plugin install aws-public --metadata region=us-east-1
First run
Section titled “First run”finfocus cost clusterGROUP CPU MEMORY TOTAL NOTESpayments 40.00 20.00 60.00 spot node priced on-demandsearch 2.50 2.50 5.00__idle__ 0.00 0.00 7.00__cluster__ 73.00 0.00 73.00
Mode: run-rate (monthly, 730 h)Total: $145.00 USDIdle: $7.00 (4.8%)Policy: built-in defaults · 3f2a9c1b4d5eThe footer reports the mode and period, the priced total, idle cost and its share, the policy source with a short digest, and any resources that could not be priced.
Grouping
Section titled “Grouping”--group-by selects the aggregation dimension (default namespace):
namespace— one group per namespacecontroller—namespace/controller_kind/controller(for examplepayments/Deployment/api)pod—namespace/podnode— node name; idle rows group under their nodelabel:<key>— the value of pod label<key>; pods without the label group under<none>and are never droppedpulumi-stack—<stack>/<project>parsed from the pod annotationfinfocus.dev/pulumi-urn. A missing or malformed URN groups under<none>and the pod is still counted. JSON and NDJSON groups list those URNs inpulumi_urns
__idle__ (unclaimed node capacity) and __cluster__ (shared infrastructure
such as the control plane) always stay separate groups. Groups sort by total
cost descending.
finfocus cost cluster --group-by controllerfinfocus cost cluster --group-by label:teamfinfocus cost cluster --group-by pulumi-stackUse --context to pick a kubeconfig context, and --selector key=value
(repeatable) to restrict pods by label.
Namespace scope
Section titled “Namespace scope”--namespace payments restricts allocation to one namespace. Workload rows
are exact for that namespace; the __idle__ and __cluster__ rows are
omitted and the footer says so (Idle: omitted (--namespace scoped)).
Allocation policy
Section titled “Allocation policy”The allocator’s policy is resolved, first found wins (files never merge):
--policy <file>$PROJECT/.finfocus/allocation.hujson(project config dir)~/.finfocus/allocation.hujson(global config dir)- none — the plugin’s built-in defaults
Files are HuJSON (comments and trailing commas allowed), standardized before
being passed to the allocator. A discovered file that fails to parse is a
fatal error — FinFocus never silently falls back to defaults. See
plugins/kubernetes/README.md for the policy fields.
idle and system_workloads default to separate. Set either to share
to fold unused node capacity, or kube-system and DaemonSet cost, into the
other workloads on the same node. The formula is locked in
specs/614-allocator-policy-v2/spec.md. --show-policy prints the
effective policy JSON, including whichever of those values is in effect,
and a digest that changes when they change.
Print the effective policy (defaults plus overrides) and its digest without contacting a cluster:
finfocus cost cluster --show-policyEvery report footer shows the policy source and digest, so any number can be traced back to the exact policy that produced it.
Automation: JSON, NDJSON, and MCP
Section titled “Automation: JSON, NDJSON, and MCP”--output json emits a single document with mode, period, currency,
group_by, total, idle (omitted when namespace-scoped),
namespace_scoped, incomplete, groups, priced, policy, and
warnings. In historical mode, priced[].monthly is the window total
(TotalCost), not a 730-hour projection. Each group may include pulumi_urns when a workload in it
carries annotation finfocus.dev/pulumi-urn. --output ndjson emits a summary line followed by one group
line per group. The command is also exposed as an MCP tool (finfocus mcp-server), so agents can call it like any other read-only command.
Price workloads declared in a Pulumi plan
Section titled “Price workloads declared in a Pulumi plan”cost cluster prices a running cluster. Before a workload is deployed, the
same plugin can estimate it from the plan: cost projected prices
Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs from their declared
resource requests and rates you set. No cluster connection is needed.
export FINFOCUS_KUBERNETES_CPU_HOURLY_RATE=0.04 # USD per vCPU-hourexport FINFOCUS_KUBERNETES_MEMORY_GIB_HOURLY_RATE=0.005 # USD per GiB-hourexport FINFOCUS_KUBERNETES_DAEMONSET_NODE_COUNT=4 # optional, DaemonSetsexport FINFOCUS_KUBERNETES_JOB_HOURS_PER_MONTH=10 # optional, Jobs and CronJobs
pulumi preview --json > plan.jsonfinfocus cost projected --pulumi-json plan.jsonA Deployment with replicas: 3 and one container requesting cpu: 500m and
memory: 1Gi costs 3 × (0.5 × 0.04 + 1 × 0.005) × 730 = 54.75 USD a month.
The note on each result names the method and the pod count source.
Without the rates, each workload shows NO_COST_DATA with a note naming the
missing variable, never a $0 price. A DaemonSet without a node count, or a
Job or CronJob without hours, is reported the same way. Other kubernetes:*
types such as ConfigMap stay declined.
These are estimates from configured rates, useful for comparing changes in a
pull request. cost cluster remains the authoritative number for a running
cluster. The variables, the decline reasons, and the request rules are in the
plugin README.
Historical window
Section titled “Historical window”--from and --to price actual spend over that window. Both accept
2006-01-02 and RFC3339. --to defaults to now when only --from is set.
Select Prometheus when more than one usage source is installed:
export FINFOCUS_PROMETHEUS_URL=http://127.0.0.1:9090finfocus cost cluster \ --usage-source prometheus \ --from 2026-09-28 \ --to 2026-10-05The footer mode is historical and the period is the window and its
length, for example historical (2026-09-28 to 2026-10-05, 7 days). A
window that is not whole UTC days prints RFC3339 bounds and hours. A run with no window stays run-rate (monthly, 730 h).
When Prometheus holds no data for the start of the window, for example
because its retention is shorter than the window, the report is marked
incomplete with a warning naming the time stored data starts. The missing
part is not counted as zero usage.
Prometheus must be scraping cAdvisor and kube-state-metrics, including the
node-label metrics kube_node_labels and kube_node_info. Outside a
cluster, set FINFOCUS_PROMETHEUS_URL. Inside a cluster, an unset URL uses
the Prometheus Operator service. The
plugin README
lists the address, the bearer token, and the install gate.
Pod labels come from kube-state-metrics, which exports only the labels named
in its --metric-labels-allowlist flag and replaces every character that is
not a letter, digit, or underscore with _. A historical --group-by uses
that recorded key: app.kubernetes.io/name is
label:app_kubernetes_io_name, and --group-by label:app.kubernetes.io/name
puts every pod under <none>. The --selector flag accepts either form.
Limitations
Section titled “Limitations”- The kubernetes usage source still rejects a window with its run-rate-only error. A window needs a source that reports historical mode.
- On the no-window path, spot nodes are priced on-demand. Each EKS Fargate pod
is priced on its own from its vCPU and memory request when the pricing
plugin returns a positive monthly cost; otherwise the pod is a $0 row with
a note. kind cannot simulate Fargate, so
make test-e2e-kinddoes not cover that path. The rates live in finfocus-plugin-aws-public#409. - A node whose price resolves to
$0(for example an unknown instance type in aws-public) is treated as unpriced, never as free; if no node can be priced at all the command fails.