Platform Engineering in Depth · FinOps & Cost Optimization

FinOps & Cost Optimization

The cloud made compute a variable cost — you can conjure a thousand CPUs at 3am and hand them back by breakfast — and that superpower is exactly why the bill spirals. On a shared Kubernetes cluster the problem gets worse: the invoice arrives as a wall of instance-hours with no idea which team, which service, or which feature spent the money. FinOps is the discipline that closes that loop — giving engineers timely, accurate cost data so the people who cause spend can own it. This is the architect-level tour: the operating model, how to actually allocate a Kubernetes bill, right-sizing, buying compute cheaper, killing waste, guardrails that stop overspend before it happens, and the sustainability angle that is quietly becoming the same conversation.

☺ Explain it like I’m 10

Imagine your whole class shares one giant pizza every day, and at the end of the month one big bill arrives that just says “pizza: $900.” Nobody knows who ate what, so nobody tries to waste less — some kids order three slices and eat one, and the leftovers go in the bin. FinOps is the friendly helper who weighs each person’s plate, shows everyone “you took three slices but only ate one,” and gently says “next time, take one — and we’ll buy the cheese in bulk because we know we always need it.” Nobody starves, the bin stops filling up, and the bill shrinks — not by eating less pizza, but by stopping the waste.

🦥Your host for this topic: Sol the Sloth — slow, deliberate, and allergic to waste. Sol never guesses at a number; Sol measures first, right-sizes second, and only then buys the cheaper compute. When Sol is teaching, we’re counting every CPU-second and asking the one question that runs through this whole page: who is this for, and are we actually using what we pay for?

FinOps & the operating model

☺ Like you’re 10: FinOps isn’t a money-saving gadget. It’s a habit the whole team shares — see the spending, trim the waste, then keep it trimmed, on repeat.

The single biggest misconception is that FinOps is a cost-cutting project you run once. It isn’t. FinOps is a cultural operating model — a way of working where engineering, finance, and product make continuous, data-driven trade-offs about cloud spend, the same way DevOps made them make continuous trade-offs about reliability. The goal is not to spend the least; it is to get the most business value per dollar. Sometimes the right FinOps decision is to spend more — because that feature ships faster or that customer pays for it.

Visibility → optimization → accountability

Every FinOps program rests on three pillars, in order, because each one depends on the last. Visibility comes first: you cannot optimise a number you cannot see, so the founding act is making spend observable, broken down by the things engineers actually control. Optimization is next: with the numbers visible, you right-size, kill waste, and buy cheaper compute. Accountability is the pillar that makes it stick — spend data flows to the teams who create it, budgets and unit-cost targets become part of how a team is run, and cost stops being “someone in finance’s problem.” Skip visibility and optimisation is guesswork; skip accountability and every gain quietly erodes as new waste creeps back in.

The FinOps Foundation phases: Inform, Optimize, Operate

The FinOps Foundation formalises this as a continuous lifecycle with three phases you cycle through, not a straight line you finish. In Inform, you build allocation, showback, and shared visibility so everyone sees accurate, timely cost data mapped to their world. In Optimize, you act on it — right-sizing, deleting idle resources, taking commitment discounts, choosing cheaper architectures. In Operate, you make it business-as-usual: define governance, set budgets and policies, measure against unit-economics targets, and continuously improve. A mature organisation runs all three at once — informing on this quarter’s spend while optimising last quarter’s findings and operating the guardrails from the quarter before.

◆ Key idea

FinOps is Inform → Optimize → Operate, forever — and the throughline is that it’s a team sport. Finance brings the business context and the commitments; engineering brings the levers (requests, replicas, instance types) that actually move the number. Neither can do it alone, which is why a central FinOps or platform team enables decisions but doesn’t make them all.

Engineers own their spend

The principle that most distinguishes FinOps from old-school cost control is decentralised ownership. In the old model, a central team hunted for savings after the fact and begged teams to change. In FinOps, the engineers who write the manifests own the cost those manifests create — because they are the only ones with a fast enough feedback loop to act. That only works if the data reaches them quickly and in their language: a Slack alert saying “your namespace’s cost jumped 40% after yesterday’s deploy” lands, where a PDF the finance team reads next month does not. The platform’s job (this is where you come in) is to make that fast, self-serve cost feedback a built-in feature of the developer experience, not a report someone has to request.

Cost visibility on Kubernetes

☺ Like you’re 10: The cloud bill tells you how many tables you rented at the restaurant. It says nothing about which friends sat at them. On a shared cluster, that’s the whole problem.

Cloud providers bill you by infrastructure: this many instance-hours of m6i.4xlarge, this many GB of disk, this much egress. That’s perfectly useful when one app owns one VM. It becomes almost useless the moment you run a shared Kubernetes cluster, because a single node runs pods from a dozen teams at once. The bill knows the node cost; it has no idea that 40% of that node was the payments team and 15% was an idle buffer nobody used. Turning “node-hours” into “team X spent $Y” is the core technical problem of Kubernetes cost visibility.

Why a bill-by-instance is useless on shared clusters

Consider one $500/month node running twenty pods from six teams. The cloud invoice reports one line: $500. To make that actionable you must disaggregate it down to the pod, then re-aggregate it up by whatever dimension a human cares about — team, namespace, service, environment, customer. Doing this correctly means knowing each pod’s share of CPU, memory, GPU, disk, and network for every minute it ran, multiplied by that node’s real hourly rate (which itself varies with spot vs on-demand vs a commitment discount). No cloud cost console does this for you at pod granularity, which is exactly the gap the Kubernetes cost tools fill.

OpenCost & Kubecost

OpenCost is the CNCF project that defines an open specification and reference implementation for Kubernetes cost allocation. It scrapes resource metrics (usually from Prometheus and the kubelet), joins them to real cloud billing rates via the providers’ pricing APIs, and produces a per-pod, per-namespace, per-label cost breakdown you can query through an API or CLI. Because it’s an open spec, it gives you a vendor-neutral answer to “what does this workload cost?” that works across AWS, GCP, Azure, and on-prem.

Kubecost is the commercial product built on top of OpenCost. It adds a polished UI, long-term retention and reporting, savings recommendations, alerting, multi-cluster aggregation, and enterprise features like SSO. The mental model: OpenCost is the measurement engine and open standard; Kubecost is the batteries-included platform around it. For the exam and for most platforms, know that OpenCost is the allocation core and Kubecost is one way to consume it — you can also read OpenCost’s data straight into Grafana or your own observability stack.

# The kubectl-cost plugin queries OpenCost/Kubecost for a real allocation.
# "efficiency" = how much of what you PAID FOR you actually USED.
$ kubectl cost namespace --window 7d --show-efficiency

NAMESPACE      CPU      RAM      PV      EFFICIENCY   TOTAL/mo
payments      $214.80  $96.20   $40.00     31%       $351.00
search        $180.10  $128.40  $12.00     58%       $320.50
platform      $ 92.30  $ 44.10  $ 8.00     22%       $144.40
idle (unallocated node capacity) .............         $308.90
shared (monitoring, ingress, DNS) ............         $ 96.70
-----------------------------------------------------------------
                                                     $1,321.50
⚠ Watch the two hidden lines

The eye goes to the named teams — but the two biggest optimisation targets are usually the unnamed rows. Idle ($308.90) is node capacity you rented and reserved but no pod ever used. Shared ($96.70) is genuine cluster overhead — the monitoring, ingress, and DNS every team depends on — which must be fairly split back onto teams or it becomes an accountability black hole. A cost report that hides idle and shared spend is telling you a comforting lie.

Allocating shared, idle & overhead cost

A node’s cost splits into three buckets, and how you handle each is a real design decision. Allocated cost is the share attributable to a specific pod, priced (in OpenCost’s model) on the greater of what the pod requested or actually used for CPU and memory — because a request reserves capacity nobody else can have, so you pay for it whether you use it or not. Idle cost is node capacity that was neither requested nor used; it’s the price of the gap between what you provisioned and what your workloads claimed, and it’s where autoscaling and bin-packing earn their keep. Shared/overhead cost is cluster-wide infrastructure (control plane, DaemonSets, the ingress controller) that no single tenant owns; you redistribute it back to teams either evenly, weighted by their usage, or by a custom split — and each choice creates different incentives.

Cloud bill 1 node $500 / mo 🦥 allocate per-pod metrics payments · $205 search · $120 platform · $58 idle · $77 shared · $40 The invoice sees one $500 line · the allocation engine sees who spent it — and what nobody used

The requests-vs-usage gap

The most important number in all of Kubernetes FinOps is the gap between requests (what a pod reserved) and actual usage (what it burned). You pay the cloud for the node; the scheduler carves that node up by requests; so every CPU a pod requested but didn’t use is capacity that is both billed to you and blocked from anyone else. A cluster where pods request 4 CPUs and use 0.8 is running at 20% efficiency and paying full price for the other 80%. Almost every optimisation later on this page — right-sizing, bin-packing, autoscaling — is ultimately an attack on this one gap.

Allocation: showback vs chargeback

☺ Like you’re 10: Showback is putting everyone’s pizza-plate weight on the fridge so they can see it. Chargeback is actually splitting the bill and making each person pay their share.

Once you can allocate cost to a team, you have to decide what to do with that number. There are two models, and choosing between them is as much about culture as accounting.

Showback vs chargeback — and when to use each

Showback reports each team their cost without moving any money — the finance ledger is untouched, but everyone can see what they spend. Chargeback actually bills the cost back to the team’s or business unit’s budget, so cloud spend hits their P&L directly. Showback is where nearly everyone should start: it creates awareness and behaviour change with almost no political friction, and it lets you find and fix the inevitable allocation gaps (unlabelled resources, disputed shared costs) before anyone’s budget depends on them. Chargeback creates the strongest accountability — real money concentrates the mind — but it demands near-perfect allocation accuracy and organisational maturity, because now every misattributed dollar is an argument. A common path is showback for a year, then chargeback once the data is trusted.

DimensionShowbackChargeback
Money moves?No — informational onlyYes — hits the team’s budget
AccountabilityModerate (awareness)Strong (real financial impact)
Allocation accuracy needed“Good enough” to be credibleNear-perfect — disputes cost money
Political frictionLow — easy to introduceHigh — needs buy-in & maturity
Best forStarting out; changing behaviourMature orgs; true cost ownership

Slicing by namespace, label, tenant & team

Allocation is only as good as your metadata. The mechanism is Kubernetes labels and namespaces: a cost tool groups spend by whatever labels you attach, so a disciplined labelling convention — team, cost-center, env, app — is the foundation of the entire practice. Namespaces give you a natural coarse boundary (often one per team or tenant); labels let you slice finer, across namespaces, by service or environment. The failure mode is unlabelled resources, which pile into an “unallocated” bucket that grows until nobody trusts the report. This is why mature platforms enforce labels at admission (more on that under guardrails) rather than hoping engineers remember.

# Cost allocation is downstream of labels. This Deployment is self-describing:
# a tool can attribute its spend to a team, cost-centre, env, and app instantly.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: payments
  labels:
    team: payments
    cost-center: cc-4021
    env: prod
    app: checkout
spec:
  template:
    metadata:
      labels:            # pod labels are what the cost engine actually reads
        team: payments
        cost-center: cc-4021
        env: prod
        app: checkout
    spec:
      containers:
        - name: app
          image: registry.acme.io/checkout:1.8.3
          resources:
            requests: { cpu: "500m", memory: "512Mi" }   # ← what you PAY for
            limits:   { cpu: "1",    memory: "1Gi" }

Unit economics: cost per customer, request & feature

Total spend is a vanity metric; a bill that doubles is fine if revenue tripled. The number that matters is unit cost — cloud spend divided by a business unit that means something: cost per customer, per thousand requests, per order processed, per active tenant, per feature. Unit economics is what lets a platform tell the difference between healthy growth and a runaway inefficiency, and it’s the metric executives actually understand. Concretely: take the allocated cost of a service, divide by its business throughput (from your metrics), and track the trend. If cost-per-request is flat while traffic grows, you’re scaling efficiently; if it climbs, something is wrong regardless of what the total says.

🦆 Dot’s-eye view

“I don’t want to become a cost accountant — I want to ship. But I do want a little badge on my service’s dashboard that says ‘$0.0004 per request, up 12% this week.’ That I can act on: it tells me my last change got expensive, in a unit I understand, without anyone forwarding me a spreadsheet. Make cost a signal in my workflow, not a meeting.”

Right-sizing: you pay for requests, not usage

☺ Like you’re 10: If you reserve a whole party room for 30 kids and 4 show up, you still pay for the room for 30. Right-sizing is booking the room you’ll actually fill.

Right-sizing is the highest-leverage, lowest-risk optimisation in Kubernetes, because it directly attacks the requests-vs-usage gap and needs no new infrastructure — just better numbers in your manifests. The mechanism is simple to state and easy to get wrong: match each workload’s requests to what it genuinely needs, so the scheduler packs nodes tightly and you stop paying for reserved-but-idle capacity.

Requests, limits & QoS classes

Two settings drive everything. A request is a reservation the scheduler uses to place the pod and guarantee capacity — it is what you effectively pay for. A limit is a ceiling the runtime enforces at execution time. The relationship between them assigns each pod a Quality of Service class: Guaranteed (requests equal limits — evicted last, most predictable), Burstable (requests below limits — the pragmatic default), and BestEffort (nothing set — first to be killed under pressure). The subtlety that trips people up: a memory limit is hard — exceed it and the kernel OOMKills your container — while a CPU limit is enforced by throttling, so an over-tight CPU limit silently slows your app instead of crashing it. Many teams set memory requests equal to limits (to avoid OOM surprises) but leave CPU limits off or generous, letting bursty workloads use spare cycles.

⚠ Zero requests is not “free”

The tempting shortcut — set requests very low or to zero so a cost tool shows your team spending almost nothing — is a trap. Zero-request pods are BestEffort: the scheduler over-packs the node, and the moment it runs hot the kernel evicts your pods first. You didn’t save money; you moved the cost from “predictable dollars” to “3am incident.” Right-sizing means requesting accurately, not requesting nothing.

VPA recommendations

You should not eyeball right-sizing across hundreds of workloads — you should measure. The Vertical Pod Autoscaler (VPA) observes a workload’s real CPU and memory consumption over time and recommends requests (typically targeting a high percentile of observed usage plus headroom). Crucially, VPA has modes: Off only produces recommendations (the safest — you review and apply them in Git), while Auto/Recreate will evict and restart pods to apply new requests, which causes disruption and must be used carefully. The standard platform pattern is to run VPA in recommendation mode cluster-wide, surface its numbers to teams, and let them adopt the change through their normal GitOps flow rather than letting VPA mutate prod live.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: checkout-vpa
  namespace: payments
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: checkout
  updatePolicy:
    updateMode: "Off"          # recommend only — never evict prod pods behind our back
  resourcePolicy:
    containerPolicies:
      - containerName: app
        minAllowed: { cpu: "100m", memory: "128Mi" }
        maxAllowed: { cpu: "2",    memory: "2Gi" }
# kubectl describe vpa checkout-vpa → Recommendation:
#   Target:      cpu: 240m   memory: 410Mi   ← adopt THIS in Git (was 500m / 512Mi)
⚠ VPA and HPA fight over CPU

Do not point VPA and the Horizontal Pod Autoscaler at the same resource. HPA adds replicas when CPU is high; VPA raises the CPU request when CPU is high — aim both at CPU and they oscillate against each other. The safe division of labour: VPA sizes memory requests, HPA scales replicas on CPU or a custom metric. Right-size vertically, scale horizontally — not both on one signal.

Compute optimization: buy the same work for less

☺ Like you’re 10: Once you’re only buying the room you’ll fill, the next trick is buying it on sale — cheaper seats for work that can handle a surprise, and a bulk discount for the seats you always need.

Right-sizing shrinks how much compute you buy. Compute optimisation shrinks what each unit costs. These stack: a right-sized workload on discounted compute is the double win. The levers are spot capacity, commitment discounts, tighter bin-packing, and cheaper CPU architectures.

Spot & preemptible with interruption handling

Spot (AWS) / preemptible (GCP) / Spot VMs (Azure) instances sell the cloud’s spare capacity at up to ~70–90% off on-demand, with one catch: the provider can reclaim them on short notice (AWS gives a two-minute warning). That makes them perfect for interruption-tolerant work — stateless services with several replicas, batch and CI jobs, data processing — and wrong for anything that can’t survive a sudden node death, like a single-replica database primary. To use spot safely you handle interruption: run a node termination handler that catches the reclaim signal and cordons/drains the node gracefully, spread replicas with Pod Disruption Budgets and topology spread so a reclaim never takes them all at once, and keep a small on-demand baseline for workloads that must never blink.

Committed-use discounts, savings plans & reserved

Spot is for the spiky, disposable part of your load. For the steady baseline — the capacity you know you’ll run 24/7 for the next year — you pre-commit and take a discount. The instruments vary by provider but rhyme: Reserved Instances and Savings Plans on AWS, Committed Use Discounts on GCP, Reservations on Azure. You commit to a level of spend or usage for one or three years (paid up-front, partial, or monthly) in exchange for up to ~72% off. The FinOps discipline is to size commitments to your trough, not your peak: cover the baseline you’re confident about with commitments, serve the predictable daytime bulge with on-demand, and let spot soak up the disposable spikes. Over-commit and you pay for reservations you don’t use; under-commit and you leave the discount on the table.

Purchase modelDiscount vs on-demandRisk / catchUse it for
On-demand0% (baseline)None — pay list priceUnpredictable, short-lived load
Spot / preemptible~70–90% offCan be reclaimed with ~2 min noticeStateless, batch, fault-tolerant work
Savings Plan / CUD~30–66% off1–3 yr spend commitmentFlexible steady baseline across types
Reserved Instanceup to ~72% off1–3 yr, tied to instance familyVery stable, known long-term footprint

Bin-packing & Karpenter consolidation

The scheduler decides how tightly pods pack onto nodes, and the node autoscaler decides how many nodes exist — together they set your idle cost. Karpenter is the modern approach: instead of pre-defined node groups, it provisions right-sized nodes on demand for exactly the pending pods, picking the cheapest instance type (and spot where allowed) that fits. Its killer feature for FinOps is consolidation: it continuously watches for pods that could fit onto fewer or cheaper nodes, then reschedules them and terminates the emptied nodes — actively squeezing the idle bucket down instead of leaving half-empty nodes running. This is the automated, always-on version of bin-packing, and it pairs naturally with the deeper treatment in Scaling & Scheduling.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general-spot
spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized   # repack & delete idle nodes
    consolidateAfter: 30s
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]             # prefer spot, fall back safely
        - key: kubernetes.io/arch
          operator: In
          values: ["arm64", "amd64"]                # let Graviton win on price
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["c7g", "m7g", "c6i", "m6i"]

ARM / Graviton

The last compute lever is the CPU architecture itself. ARM-based instances — AWS Graviton, GCP Axion/Tau, Azure Cobalt — typically deliver ~20–40% better price-performance than comparable x86 for many workloads. The cost of admission is a multi-architecture container image: your images must be built for arm64 (a multi-arch manifest so the same tag runs on either), and any binary dependency with native code must have an ARM build. For most modern, interpreted, or Go/Rust services the switch is nearly free once your build pipeline produces multi-arch images; for a legacy app with an x86-only dependency it may be off the table. Let a scheduler like Karpenter place pods on whichever architecture is cheapest and available.

Eliminating waste

☺ Like you’re 10: Some of the bill isn’t “too big a room” — it’s rooms you rented and then forgot about entirely. Nobody’s even in them.

Right-sizing tunes running workloads; this is about the spend that buys nothing at all. Waste hides because deleting things feels risky, so orphaned resources accumulate for years. A recurring sweep — automated where possible — is one of the fastest paths to real savings, and unlike right-sizing it often has zero performance downside because the resources aren’t serving anyone.

Idle & orphaned resources

The classic offenders: unattached persistent volumes (the pod was deleted but its disk lingers, billed monthly forever), orphaned load balancers and public IPs left behind by a deleted Service, old snapshots and disk images nobody prunes, and idle namespaces from experiments that shipped or died. Each is invisible on a dashboard of running pods because it isn’t a running pod — you have to go looking. The fix is a scheduled audit: reconcile every volume, IP, and load balancer against a live owner, and flag or garbage-collect anything unclaimed. Set volume reclaim policies deliberately so deleting a workload cleans up its disk rather than orphaning it.

Zombie workloads & over-replication

Zombie workloads are pods that run, consume, and cost — but serve no traffic and no purpose: the abandoned staging copy, the cron job whose downstream was decommissioned, the “temporary” debug deployment from last quarter. Over-replication is subtler: a service pinned at 20 replicas “for safety” that comfortably serves peak on 6, or a fixed replica count that never scales down at night. Both are found by joining cost data to traffic data — a workload that costs money while its request rate sits at zero is a zombie; a workload whose utilisation never rises above a fraction of its replicas is over-provisioned. This is exactly where cost visibility and observability must be the same conversation.

Dev clusters running overnight & scheduled scale-down

Non-production environments are a huge, easy win because nobody uses them two-thirds of the day. A dev or staging cluster running 24/7 is paying full price for nights, weekends, and holidays when every engineer is asleep — roughly 70% of the hours in a week. Scheduled scale-down fixes this: scale non-prod workloads (or whole node pools) to zero outside working hours and back up before people log on. Tools like kube-downscaler read an annotation and enforce an uptime window per namespace; a plain CronJob can do the same. Done across every non-prod environment, this alone can cut their bill by more than half.

# kube-downscaler: run dev only during working hours, in the team's timezone.
# Outside the window it scales Deployments to zero — no traffic, no cost.
apiVersion: v1
kind: Namespace
metadata:
  name: dev-payments
  annotations:
    downscaler/uptime: "Mon-Fri 08:00-19:00 Europe/London"
    downscaler/downtime-replicas: "0"
---
# Equivalent do-it-yourself version: a nightly CronJob that parks the namespace.
apiVersion: batch/v1
kind: CronJob
metadata: { name: park-dev, namespace: dev-payments }
spec:
  schedule: "0 19 * * 1-5"        # 19:00 Mon–Fri
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: scale
              image: bitnami/kubectl:latest
              command: ["kubectl","scale","deploy","--all","--replicas=0","-n","dev-payments"]
Waste typeHow to detect itThe fix
Requested-but-unused capacityLow request-vs-usage efficiencyRight-size with VPA; Karpenter consolidation
Unattached volumes / IPs / LBsResource with no live ownerScheduled audit; reclaim policies; GC
Zombie workloadsCost with zero request rateDelete; add TTLs to ephemeral envs
Over-replicationUtilisation far below replica countHPA with sane min/max; lower fixed counts
Non-prod running 24/7Steady cost, no off-hours dipScheduled scale-down; ephemeral PR envs

Budgets, forecasting & guardrails

☺ Like you’re 10: It’s cheaper to put a fence at the top of the cliff than an ambulance at the bottom. Guardrails stop the giant bill before it happens.

Everything so far reacts to spend that already happened. The Operate phase is about getting ahead of it — catching anomalies early, forecasting where you’re headed, and putting policy in the path of the deploy so waste can’t be created in the first place. This is where FinOps and governance become the same muscle.

Budgets & anomaly alerts

A budget is a threshold with an alert: AWS Budgets, GCP Budgets, Azure Cost Management all let you set a monthly target per account, tag, or project and fire notifications at 50/80/100% of it — and, importantly, forecasted to exceed, so you hear about a runaway before month-end, not after. Anomaly detection goes further: services like AWS Cost Anomaly Detection learn your normal spend pattern and alert on a statistically unusual jump — the kind a fixed threshold misses because it’s still “within budget” but is a sharp, sudden change (a runaway loop spinning up nodes, a misconfigured autoscaler, a leaked credential mining crypto). The platform move is to wire these alerts to the owning team’s channel, tied back to your allocation labels, so the alert reaches whoever can actually fix it.

Cost policies at admission

The strongest guardrail rejects waste at the door. Using a policy engine like Kyverno or OPA Gatekeeper as an admission controller, you can make cost-hostile manifests simply fail to apply: require every pod to set resource requests and limits (no free-riding BestEffort in prod), mandate the team and cost-center labels (so nothing lands in the unallocated bucket), cap the maximum replicas or requested size a single workload can claim, and block the :latest tag. Because this runs at admission, an engineer gets the feedback in their pipeline in seconds — long before anything runs and bills. Enforcing allocation labels here is what keeps the whole visibility story from rotting over time.

# Kyverno: reject any prod pod that doesn't declare requests AND cost labels.
# A missing request means unpredictable cost; a missing label means unallocated spend.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: require-cost-hygiene }
spec:
  validationFailureAction: Enforce      # block, don't just warn, in prod
  rules:
    - name: require-requests-and-labels
      match:
        any:
          - resources: { kinds: ["Pod"], namespaces: ["*-prod"] }
      validate:
        message: "Prod pods must set CPU/memory requests and team + cost-center labels."
        pattern:
          metadata:
            labels:
              team: "?*"                 # must be non-empty
              cost-center: "?*"
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "?*"
                    memory: "?*"

Quotas as cost control

Before a policy engine, Kubernetes ships two blunt-but-effective cost limiters. A ResourceQuota caps the total requests, limits, and object counts a namespace may consume — a hard ceiling on how much a single team can cost, and a natural place to encode a budget in resource terms. A LimitRange sets per-pod defaults and maximums, so a container that forgets to set requests gets a sensible default instead of running unbounded, and one that asks for 64 CPUs is refused. Together they turn “a team can accidentally spin up unlimited compute” into “a team has a defined envelope,” which is both a reliability guardrail and a cost one.

apiVersion: v1
kind: ResourceQuota
metadata: { name: team-budget, namespace: payments }
spec:
  hard:
    requests.cpu: "50"          # this namespace may reserve at most 50 CPUs...
    requests.memory: 100Gi
    limits.cpu: "100"
    limits.memory: 200Gi
    persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata: { name: sane-defaults, namespace: payments }
spec:
  limits:
    - type: Container
      default:        { cpu: "500m", memory: "512Mi" }   # applied if none given
      defaultRequest: { cpu: "200m", memory: "256Mi" }
      max:            { cpu: "4",    memory: "8Gi" }       # nothing bigger allowed
◆ Key idea

Guardrails invert the FinOps loop from detective to preventive. Budgets and anomaly alerts catch overspend fast; admission policies and quotas stop whole classes of overspend from ever being created. The best cost saving is the one you never have to find, because the platform wouldn’t let it happen — and the same mechanisms live in Governance & Compliance.

GreenOps & sustainability

☺ Like you’re 10: Wasting compute doesn’t just waste money — it burns electricity for nothing. Using less, and using it when the grid is cleanest, saves both dollars and carbon.

FinOps and sustainability are converging, because the same idle CPU that wastes a dollar also burns watts and emits carbon. GreenOps applies the FinOps mindset to environmental impact: make efficiency and carbon visible, optimise them, and operate against targets — with the happy property that most cost optimisations are also carbon optimisations. Right-sizing, killing zombies, and bin-packing reduce your bill and your footprint at the same time.

Carbon-aware scheduling

Not every workload has to run now. The carbon intensity of the electricity grid varies by hour and by region — solar and wind make some hours far cleaner than others. Carbon-aware scheduling exploits this by shifting flexible, deferrable work (nightly batch jobs, ML training, report generation) to times or regions where the grid is greenest. Projects in the CNCF ecosystem — carbon-aware KEDA scalers and the like — read a live grid carbon-intensity signal and hold or release work accordingly. It’s the same idea as spot-vs-baseline, one abstraction up: match the timing of interruption-tolerant work to when the resource (clean power) is cheapest.

Efficiency as a first-class metric

What gets measured gets managed. Making efficiency — usage over what you provisioned — a headline metric alongside latency and error rate changes team behaviour, because a chronically 20%-efficient service becomes as visibly “broken” as a slow one. Tools like Kepler (Kubernetes-based Efficient Power Level Exporter) estimate per-pod energy consumption using kernel counters and export it to Prometheus, so you can put watts and estimated carbon on the same dashboards as cost. The point is cultural: when efficiency is a first-class number, engineers optimise it as a matter of course rather than only during a cost-panic.

The Kubernetes efficiency loop

Everything on this page composes into one continuous loop, and it’s deliberately the same shape as the FinOps lifecycle. You measure real usage and cost (OpenCost, Prometheus, Kepler), allocate it to teams and units, right-size and consolidate to close the requests-vs-usage gap, buy the remainder cheaply (spot, commitments, ARM), and guard the gains with policy and budgets — then measure again, because new workloads and new waste arrive every day. Cost efficiency, like reliability and security, is never “done”; it’s a loop the platform runs forever on the whole organisation’s behalf.

🦥 never done 1 · Measure usage & cost 2 · Allocate to teams / units 3 · Optimize right-size · pack 4 · Buy cheap spot · commit · ARM 5 · Guard budgets · policy

That loop is a first-class platform capability — see how it fits the wider picture in Platform Architecture & Infrastructure, and how the same discipline applies to reliability in Reliability & Incidents.

🦥 Sol’s workshop · 20 min

On any cluster with a bit of load, install OpenCost (it needs Prometheus) and open the allocation view. Find your least-efficient namespace — the one with the biggest gap between requested and used. Deploy a VPA in updateMode: "Off" against its heaviest Deployment, wait for a recommendation, and compare it to the current requests. Now do the arithmetic Sol loves: if you adopted that recommendation across the namespace, how much of the “idle” line would disappear? Finally, add a ResourceQuota to that namespace sized just above real usage, and watch the next over-sized deploy get politely refused. Measure, right-size, guard — the whole loop in twenty minutes.

🎬 At the Platform Guild
🦊

Foxy: Our cloud bill just doubled. Can’t we just buy a three-year reservation for everything and be done with it?

🦥

Sol: Whoa. Never commit to a number you haven’t measured. Half that cluster is idle — you’d be locking in three years of paying for waste. Right-size first, then commit to what’s left.

👺

Gizmo: Easy fix — set every team’s requests to zero! Then Kubecost says nobody costs anything. Bill solved! 🤑

🐢

Timmy: That doesn’t delete the cost, Gizmo — it hides it, then makes every pod BestEffort so the kernel evicts them at 3am. I’ll add a Kyverno policy: no prod pod ships without real requests and a cost-center label.

🐘

Ellie: And I’ll put “cost per thousand requests” next to latency on every dashboard. If it climbs, that’s a bug — same as a p99 spike.

🦆

Dot: Honestly? Just show me a little “$0.0004/req, +12% this week” badge on my service. I’ll fix it myself — I just never see the number today.

🦥

Sol: That’s the whole game. Make the cost visible to the person who caused it… and they’ll shrink it faster than any spreadsheet ever could. Slowly, of course.

FinOps isn’t a bolt-on — it’s the reflex that keeps a self-service platform from becoming a self-service money fire. Give engineers the number, the levers, and the guardrails, and cost efficiency becomes something the platform delivers for free, forever. Next, see how the same guardrail-and-policy machinery generalises in Governance & Compliance, or fold these habits into your platform best practices.

🐢 Timmy’s checkpoint

1. Name the three FinOps Foundation phases, in order. 2. Why is a cloud bill by instance-hour nearly useless on a shared Kubernetes cluster, and what does a tool like OpenCost do about it? 3. What is the “requests-vs-usage gap,” and why does it drive most Kubernetes cost waste? 4. Contrast showback and chargeback — when would you start with showback? 5. You have a steady 24/7 baseline, a predictable daytime bulge, and disposable nightly batch jobs. Which purchase model fits each, and why not commit to a reservation for all of it?

Check your answers
  1. Inform (build visibility & allocation), Optimize (act — right-size, kill waste, take discounts), Operate (make it business-as-usual with budgets, policy, and unit-economics targets) — cycled continuously, not once.
  2. The invoice reports node/instance-hours, but one node runs many teams’ pods, so it can’t say who spent the money. OpenCost disaggregates node cost to each pod using per-pod resource metrics × real billing rates, then re-aggregates by namespace, label, team, or unit — including the idle and shared buckets.
  3. You pay for the node, and the scheduler carves it up by requests, but you only use actual consumption. Every CPU/GB requested-but-unused is billed and blocked from others — so a low efficiency ratio is pure waste. Right-sizing, bin-packing, and autoscaling all attack this gap.
  4. Showback reports cost without moving money; chargeback bills it to the team’s budget. Start with showback: it changes behaviour with low friction and lets you fix allocation gaps before real money — and real arguments — depend on the numbers.
  5. Commitment (Savings Plan / Reserved / CUD) for the 24/7 baseline, on-demand for the predictable daytime bulge, spot/preemptible for the disposable batch. Don’t reserve all of it: you’d pre-pay for peak and idle capacity you don’t always run, wasting the discount — size commitments to the trough.