Kubernetes
Overview
Infracost estimates the cost of Kubernetes workloads defined in plain YAML manifests, Helm charts, and Kustomize projects. Manifests are read statically — no cluster connection is needed — and Kubernetes resources appear in cost estimates, pull request comments, and governance policies alongside your Terraform, CloudFormation and other resources.
Infracost auto-detects Kubernetes projects at the directory level. A directory is detected as a Kubernetes project when it is:
- a Helm chart root (contains a
Chart.yaml), - a Kustomize directory (contains a
kustomization.yaml), or - a directory containing at least one Kubernetes manifest (a YAML file with
apiVersionandkind).
For plain-manifest projects, only the files directly in the project directory are parsed together; nested directories are detected as separate projects.
Supported resources
Infracost prices the controllers that own long-running capacity, plus the two resource types that provision billable cloud infrastructure directly:
| Kind | What is priced |
|---|---|
| Deployment | replicas × sum of container CPU/memory requests |
| StatefulSet | replicas × container requests, plus one volume per volumeClaimTemplate per replica |
| ReplicaSet | replicas × container requests |
| DaemonSet | node count × container requests (one pod per node, see usage values) |
| Job | completions × per-run runtime (see usage values) |
| CronJob | runs per month (from schedule) × completions × per-run runtime |
| PersistentVolumeClaim | one dynamically provisioned cloud disk, priced per GiB-month by storage class |
Service (type: LoadBalancer only) | one cloud load balancer at a fixed hourly rate |
Standalone Pods are not priced — a bare Pod has no replica or restart semantics to bill steadily. Other kinds (ConfigMaps, Secrets, HorizontalPodAutoscalers, custom resources such as Argo Rollouts, etc.) are not priced; see Limitations.
Resources are named namespace.kind.name in the output, for example prod.deployment.api or prod.statefulset.database. Resources without an explicit metadata.namespace use the default namespace.
How cost estimation works
A Kubernetes cluster bills on its underlying nodes, so there is no per-workload cloud price to look up at pull request time. Instead, Infracost applies a flat default rate to each workload's requested CPU and memory, the same approach as OpenCost's default pricing. The result is a default-rate estimate — useful for comparing workloads and catching cost changes in pull requests — not a reproduction of your cloud bill. If your cluster's node pools are defined in Terraform, those nodes are priced separately by the AWS/Azure/Google cloud prices.
Per-pod cost is calculated from the sum of each container's resource requests; when a container sets no request, its limit is used as a fallback (what the scheduler reserves is the best proxy for allocated cost). Sidecar-style init containers (restartPolicy: Always) are counted; one-shot init containers and ephemeral containers are excluded so transient startup and debug steps don't inflate the steady-state cost.
Workloads with no explicit replicas field are priced as 1 replica, and a minimum of 1 replica is always applied — including for workloads with replicas: 0.
Compute prices
CPU and memory use these hardcoded rates, applied over 730 hours/month:
| Component | Rate | Approx. monthly |
|---|---|---|
| vCPU | $0.031611 / vCPU-hour | ~$23 / vCPU / month |
| Memory | $0.004237 / GB-hour | ~$3.09 / GB / month |
These are the same blended defaults for all clouds. On serverless platforms such as AWS Fargate and GKE Autopilot, which bill per pod based on requested CPU and memory, the estimate scales one-for-one with what actually drives your bill — but the rates are still industry defaults, not those platforms' list prices, so treat the numbers as directional rather than invoice-accurate.
GPU and other extended resource requests (e.g. nvidia.com/gpu) are not priced; see Limitations.
Cloud detection
Storage and load balancer prices differ by cloud. Since manifests carry no cloud or region field, Infracost infers the cloud from annotations on your resources:
| Annotation signal | Cloud |
|---|---|
eks.amazonaws.com/*, service.beta.kubernetes.io/aws-load-balancer-*, AWS CSI provisioners | AWS |
iam.gke.io/*, cloud.google.com/*, networking.gke.io/*, GKE CSI provisioners | |
azure.workload.identity/*, Azure load balancer annotations, Azure CSI provisioners | Azure |
| No match | Blended default rates (storage also falls back to a storage class name hint) |
Storage prices
PersistentVolumeClaims and StatefulSet volumeClaimTemplates are priced per GiB-month based on the storageClassName and the detected cloud:
| Cloud | Storage classes | Default (unknown class) |
|---|---|---|
| AWS | gp3 $0.08, gp2 $0.10, io1/io2 $0.125, st1 $0.045, sc1 $0.015, standard $0.05 | $0.08 (gp3) |
pd-standard $0.04, pd-balanced/standard-rwo $0.10, pd-ssd/premium-rwo $0.17 | $0.10 (pd-balanced) | |
| Azure | standard_lrs $0.045, standardssd_lrs/managed-csi $0.075, premium_lrs/managed-premium $0.135 | $0.075 (StandardSSD_LRS) |
| Unknown | — | $0.10 (blended) |
Matching is by the storage class name as written in the manifest, case-insensitively, against the well-known names in the table. StorageClass objects themselves are not read, so a class with a custom name is priced at the detected cloud's default rate rather than the rate of the volume type its parameters actually provision — for example, an EKS StorageClass named fast-storage that provisions io2 volumes is priced at the AWS default of $0.08/GiB (the gp3 rate), not the io2 rate.
Load balancer prices
A Service with type: LoadBalancer is priced at the fixed hourly charge of the load balancer it provisions (data processing charges are not included):
| Cloud | Rate |
|---|---|
| AWS — Classic ELB (default) | $0.025 / hour |
AWS — NLB (aws-load-balancer-type: nlb or external annotation) | $0.0225 / hour |
| $0.025 / hour | |
| Azure | $0.025 / hour |
| Unknown | $0.025 / hour |
Usage values
Some costs depend on runtime state that manifests don't carry. These are read from usage values and marked as usage costs in the output:
| Kind | Usage key | Default | Meaning |
|---|---|---|---|
| DaemonSet | nodes | 1 | Number of nodes in the cluster (a DaemonSet runs one pod per node); whole number |
| Job / CronJob | runtime_hours_per_run | 0 | How long one pod runs per execution, in hours; decimals allowed, e.g. 0.25 for 15 minutes |
This means a DaemonSet is priced for a single node, and Jobs and CronJobs show zero cost, until you provide these values. For CronJobs, the runs-per-month is calculated from the schedule field (standard cron syntax including @daily-style macros, steps and ranges is supported).
Set the values in an infracost-usage.yml file, either per resource type or per resource. Kubernetes resources are addressed as namespace.kind.name:
version: 0.1
resource_type_default_usage:
daemonset:
nodes: 25 # applies to all DaemonSets
cronjob:
runtime_hours_per_run: 0.5
resource_usage:
prod.daemonset.log-agent:
nodes: 50 # overrides the default for this DaemonSet
prod.cronjob.nightly-report:
runtime_hours_per_run: 2
Usage defaults defined in Infracost Cloud (Org Settings > Usage defaults) do not support Kubernetes resource types yet — use infracost-usage.yml files for Kubernetes usage values.
Helm
A directory containing a Chart.yaml is rendered through the Helm SDK's helm template code path — client-only, with no cluster — and the rendered manifests are then priced like plain manifests. Charts render with Helm's standard no-cluster defaults: the lookup function returns an empty result (the chart's fallback values are used), and .Capabilities reports Kubernetes v1.20 with the built-in API versions, so charts that check for newer cluster versions or CRDs take their fallback branch. This is not configurable yet.
If a chart declares a required value that no values file in the repo supplies, that chart fails to render — you'll see an error naming the missing value, the whole chart contributes nothing to the estimate, and the rest of the run completes normally. Supplying the value in a values file in the repo fixes it, the same as running helm template locally.
Values files
The chart's values.yaml is always applied. A values-<env>.yaml file in the chart root (e.g. values-prod.yaml) is detected as an environment and estimated separately with that overlay applied.
Values files must be readable YAML: SOPS-encrypted or sealed values files can't be decrypted at estimate time, so replica counts or resource sizes stored in them fall back to defaults — typically pricing those workloads at or near zero. Keep cost-relevant values such as replicas and resource requests in plaintext values files; secret values never affect the estimate.
Dependencies
Subcharts vendored under charts/ are rendered and priced as part of your deployment — a bundled PostgreSQL chart shows up with its own workloads and volumes, attributed to the vendored chart files. Chart conditions and tags from Chart.yaml are honored.
Dependencies declared in Chart.yaml but not vendored are downloaded before rendering. Classic http(s) chart repositories, git:: sources, s3:// buckets, and direct archive URLs are supported. Dependencies hosted in OCI registries (oci://) can't be fetched yet — you'll see an explicit fetch error for that dependency and the rest of the chart is still estimated.
Helm hooks are included in the render. Since hooks are almost always short-lived Jobs, they show no monthly cost unless you provide runtime usage data — which matches reality, as a migration job that runs for minutes doesn't move a monthly bill.
Helmfile is not supported: charts stored in the repo are detected and priced individually as Helm projects, but helmfile.yaml itself — its releases, environments and value layering — is not read.
Kustomize
Kustomize projects are resolved statically: Infracost follows the resources:/bases:/components: chain, applies patches and transformers, and prices the result. A base plus its overlays is treated as one project with one environment per overlay. Remote bases over git, http(s), s3, and gcs are downloaded automatically.
| Feature | Support | Notes |
|---|---|---|
resources: / bases: | ✅ | Files, directories, and remote references |
components: | ✅ | |
replicas: | ✅ | |
patches: | ✅ | Strategic-merge and JSON 6902, targeted by kind/name/namespace. labelSelector/annotationSelector targets are not supported — a warning is shown and the patch is matched on kind/name only |
patchesStrategicMerge: | ✅ | $patch: delete/replace directives are not honored — a warning is shown and the patch is merged structurally |
patchesJson6902: | ✅ | add/replace/remove operations; move/copy/test are skipped with a warning |
namePrefix / nameSuffix / namespace | ✅ | |
commonLabels / labels / commonAnnotations | ✅ | Applied before tagging policies and cloud detection run |
images: | — | Ignored; doesn't affect costs |
configMapGenerator / secretGenerator | — | Ignored; generated objects aren't priced |
vars: / replacements: | ❌ | Not applied, with no warning — if you use replacements: to set replica counts or resource requests, those changes are not reflected in the estimate |
helmCharts: / plugin generators / transformers | ❌ | A warning is shown; workloads they produce aren't priced. If the chart lives in your repo, point Infracost at the chart directory itself |
Tagging policies and labels
Kubernetes labels are treated as tags, so tagging policies evaluate Kubernetes resources the same way they evaluate Terraform resources. Labels are read from the workload object's own metadata.labels — after Helm templating and Kustomize label transformers are applied, so labels added by chart helpers such as _helpers.tpl count. Labels set only on the pod template inside a workload are not read, so put governance labels on the workload's own metadata.
Annotations are not checked as tags — only labels are. Some teams record metadata such as ownership in annotations because annotation values allow characters that label values don't (for example, owner: user@acme.org is not a valid label value). Values stored in annotations are not visible to tagging policies, so any key you want a policy to enforce must be set as a label.
Tagging policies apply to every IaC type by default, so an existing organization-wide tagging policy will start evaluating Kubernetes resources as soon as Kubernetes support is enabled. Since Kubernetes tagging conventions usually differ from cloud tagging conventions, we recommend using the policy's IaC types filter to give Kubernetes projects their own tagging policy — see Kubernetes tagging policies for the recommended setup.
Limitations
- Autoscalers are not evaluated. Estimates are a single monthly figure based on the manifest's
replicasvalue; HorizontalPodAutoscaler and VerticalPodAutoscaler objects are skipped. Since Deployments managed by an HPA usually omitspec.replicas, they are priced as 1 replica — setspec.replicasto your typical steady-state count for a more representative estimate. - GPUs and extended resources are not priced. Only CPU and memory requests/limits are read;
nvidia.com/gpuand similar requests are skipped without a warning, so GPU-heavy workloads are estimated significantly below their real cost. - LimitRange defaults are not applied. Workloads whose sizes come from a namespace LimitRange instead of explicit requests show zero compute cost. Setting requests (or limits) in the workload manifest fixes this.
- Bare Pods are not priced, and neither are custom resources such as Argo Rollouts, Knative Services, or OpenShift DeploymentConfigs.
- Terraform-managed Kubernetes resources are not priced. Resources from the Terraform
kubernetesandhelmproviders (kubernetes_deployment,helm_release, etc.) are treated like any other unsupported Terraform resource type. Kubernetes pricing applies to YAML manifests, Helm charts and Kustomize projects. - Template languages are not executed. Jsonnet, ytt, cdk8s, and Pulumi programs are not run — but if your pipeline commits the rendered YAML into the repo, that output is picked up and priced like any other manifest.
- Scaled-to-zero workloads are priced as one replica, since a minimum of 1 replica is always applied.
Troubleshooting
If you run into any issues, please join our community Slack channel, we'll help you very quickly 😄
Jobs, CronJobs or DaemonSets showing zero or low costs
Common causes: Jobs and CronJobs cost $0 until you provide runtime_hours_per_run, and DaemonSets assume a 1-node cluster until you provide nodes.
Solution: Add the usage values to an infracost-usage.yml file.
Helm chart resources missing from the estimate
Common causes: A chart failed to render (e.g. a required value has no value in the repo, or a dependency couldn't be fetched) — any render error excludes the entire chart, and the error is shown in the output. Or cost-relevant values live in an encrypted values file.
Solution: Check the output for the render or fetch error; supply missing values via a values file in the repo, and keep replicas and resource requests in plaintext values files.