Education › DevOps › Stage 4: Operate & secure

Cost awareness (FinOps)

Tagging, right-sizing, autoscaling, and reading a cloud bill before it surprises you.

Advanced ~25 min read Module 17 of 17

In a data centre, spending was decided once a year by a purchasing department. In the cloud, every engineer who merges a Terraform change or sets a replica count is making a purchasing decision, usually without seeing the price. Bills grow quietly until someone in finance asks a question nobody can answer. FinOps is the practice of making cost a visible, shared engineering concern. This module gives you the working knowledge: how to attribute spend, where the waste usually is, and how to put guard rails in place.

After this module you can
  • Explain the FinOps loop of inform, optimise and operate, and why engineers own cost
  • Design and enforce a tagging policy so that every cost can be attributed to a team and a service
  • Read a cloud bill and identify the usual drivers: compute, storage, data transfer and managed services
  • Right-size workloads and tune Kubernetes requests, autoscaling and scheduling for cost
  • Choose between on-demand, committed and spot pricing, and set budgets and anomaly alerts

Cost is an engineering metric

FinOps brings engineering, finance and product together around cloud spending, with the goal of getting the most value per unit of money, not simply the lowest bill. The FinOps Foundation describes the work as a loop with three phases.

PhaseQuestionActivities
InformWhere is the money going, and who is spending it?Tagging, allocation, dashboards, showback to teams
OptimiseWhere are we paying for something we do not use?Right-sizing, deleting waste, commitments, architecture changes
OperateHow do we keep it that way?Budgets, alerts, policies, regular reviews, cost in design discussions

The ordering matters. Teams that jump to optimisation without attribution make one-off savings that quietly return, because nobody owns the number. Once every team can see what its services cost, most waste removes itself, since engineers dislike waste once they can see it.

Absolute spend is a poor measure, because a growing business should spend more. Track unit economics instead: cost per customer, per order, per thousand requests. If cost per order is falling while total spend rises, you are doing well. If it is rising, something in the architecture is scaling badly.

text
unit cost          = cost of the service / units of business value delivered
example            = monthly cost of checkout service / orders processed that month

allocation rate    = spend attributed to an owner / total spend        (aim high)
waste rate         = spend on idle or unused resources / total spend   (aim low)

Tagging: no attribution, no accountability

A tag (a label on Google Cloud and in Kubernetes) is a key and value attached to a resource. Billing data can be grouped by tag, and that is the only practical way to answer "what does the orders service cost?" Agree on a small mandatory set and enforce it mechanically.

TagExampleAnswers
teamshop-platformWho owns this and gets asked about it?
serviceorders-apiWhich application is it part of?
environmentprod, staging, devCan it be switched off at night?
cost-centercc-4100Which budget pays for it?
managed-byterraformWhere do I change it?

Do not rely on people remembering. With Terraform, the provider can apply tags to every resource it creates, so an untagged resource becomes the exception that needs explaining.

hcl
provider "aws" {
  region = var.region

  default_tags {
    tags = {
      team        = "shop-platform"
      service     = var.service
      environment = var.environment
      cost-center = "cc-4100"
      managed-by  = "terraform"
    }
  }
}
  • Enforce in CI. A policy check from the secrets module fails any plan that creates an untagged resource.
  • Activate the tags for billing. On AWS, for example, a tag must be enabled as a cost allocation tag before it appears in cost reports, and it is not applied retroactively.
  • Decide how to split shared costs, such as the Kubernetes control plane, NAT gateways and observability tooling: proportionally by usage, evenly, or kept as a central platform cost.
  • Hunt for the untagged remainder regularly. It is where forgotten experiments live.
Tip

Separate cloud accounts, subscriptions or projects per team or environment give you coarse attribution for free, even before tagging is perfect, because billing is always broken down by account.

Reading a bill

Open the cost explorer, group by service, and sort descending. In most organisations a handful of lines account for nearly all the spend, and they fall into four families.

  • Compute: virtual machines, Kubernetes nodes, functions. Usually the largest line, and the one where over-provisioning is most common.
  • Storage: object storage, disks and snapshots. Cheap per gigabyte, but it only ever grows unless lifecycle rules delete or archive old data. Look for unattached disks and years of accumulated snapshots.
  • Data transfer: the line that surprises everyone. Inbound traffic is generally free. Traffic out to the internet is charged, and so is traffic between availability zones and between regions. A NAT gateway charges per gigabyte processed on top of its hourly fee.
  • Managed services: databases, caches, queues, and above all logs and metrics. Observability ingestion and retention regularly becomes one of the top three lines, because logging everything at debug level is free to write and expensive to keep.
bash
# monthly cost by service
aws ce get-cost-and-usage \
  --time-period Start=2026-08-01,End=2026-09-01 \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE

# the same, broken down by the 'team' tag
aws ce get-cost-and-usage \
  --time-period Start=2026-08-01,End=2026-09-01 \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=TAG,Key=team
Watch out

Architecture decisions carry data-transfer costs that are invisible in a diagram. A chatty service whose replicas sit in a different zone from its database pays for every query. Pulling large container images or datasets through a NAT gateway pays per gigabyte. Private endpoints to the provider's storage and registry services usually remove that charge.

Right-sizing and Kubernetes

Right-sizing means matching what you provision to what you use. Look at utilisation over a representative period, at least two weeks, so that weekly peaks are included. A VM that peaks at fifteen percent CPU can drop a size or two. Size for the realistic peak plus headroom, and let autoscaling handle the rest, instead of provisioning for a theoretical worst case all day and all night.

In Kubernetes, cost is decided by requests, not by usage. The scheduler reserves what a pod requests, and the cluster autoscaler adds nodes when requests cannot be satisfied. A pod that requests two cores and uses a tenth of one wastes nearly two cores that nothing else can use. Multiply that across a cluster and it is common to find nodes that are fully reserved and mostly idle.

bash
kubectl top pods -n shop --containers            # what is actually being used
kubectl get pods -n shop -o custom-columns=\
NAME:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,\
MEM_REQ:.spec.containers[*].resources.requests.memory
kubectl describe node NODE_NAME | grep -A 8 'Allocated resources'
Requested CPU that is not being used, per namespace
promql
sum by (namespace) (kube_pod_container_resource_requests{resource="cpu"})
-
sum by (namespace) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))
Scale replicas with load instead of provisioning for the peak
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orders
  namespace: shop
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orders
  minReplicas: 2
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  • The HPA's utilisation target is a percentage of the pod's CPU request, so wrong requests also mean wrong scaling.
  • The Vertical Pod Autoscaler in recommendation mode suggests requests based on observed usage, which is a good starting point for right-sizing.
  • The cluster autoscaler, or Karpenter on AWS, removes nodes when pods fit on fewer, and can pick cheaper instance types.
  • Non-production can sleep. Scaling development and staging environments to zero outside working hours removes roughly two thirds of their running hours.
  • Tools such as OpenCost attribute cluster cost to namespaces and workloads, using the same labels you already apply.

Pricing models and guard rails

ModelDiscountTrade-offUse for
On-demandNoneFull flexibilitySpiky or unpredictable load; anything new
Commitment (reserved, savings plans, committed use)SubstantialYou pay for one to three years whether you use it or notThe steady baseline that runs all day, every day
Spot / preemptibleLargestCapacity can be reclaimed at short noticeStateless, fault-tolerant work: CI runners, batch jobs, extra replicas

Sequence matters. Right-size first, then commit. Committing to an over-provisioned fleet locks in the waste for years. Cover the stable baseline with commitments, handle variation on demand, and move whatever tolerates interruption to spot. Disposable, twelve-factor workloads that handle SIGTERM gracefully are exactly what makes spot capacity safe to use.

Then make surprises impossible. Set a budget per account or team with alerts at, say, fifty, eighty and one hundred percent of the expected monthly amount, plus an alert on the forecast, which warns you mid-month instead of on the invoice. Turn on anomaly detection, which flags a service whose daily spend departs from its pattern.

hcl
resource "aws_budgets_budget" "shop" {
  name         = "shop-monthly"
  budget_type  = "COST"
  limit_amount = "2000"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = ["shop-platform@example.com"]
  }
}

Finally, put cost where decisions are made. Tools such as Infracost comment on a pull request with the monthly price difference of a Terraform change, which turns cost into a normal part of code review. A short monthly review of the top movers, with the teams that own them, keeps the loop turning.

Note

Optimise in order of size. An hour spent shaving a small line item is wasted while an idle database cluster ten times its cost sits untouched. And never optimise reliability away: removing the second availability zone does cut the bill, until the day it does not.

Hands-on practice

Find the waste in an account you can see

  1. In the cloud account you used for the cloud fundamentals module, or any account you can read, open the cost explorer. Group by service for the last three months and write down the top five lines.
  2. Group the same data by tag. Work out your allocation rate: what share of spend has an owner tag? List the largest untagged items.
  3. Add default_tags to your Terraform provider block, apply, and confirm the tags appear on a resource. Activate them as cost allocation tags if your provider requires it.
  4. Search for orphans: unattached disks, old snapshots, unused load balancers, idle public IP addresses, and stopped instances that still hold storage.
  5. In your Kubernetes cluster, compare kubectl top pods with the configured requests for three workloads. Calculate how over-requested each is, and propose new values with headroom.
  6. Add a HorizontalPodAutoscaler to one Deployment, generate load, and watch it scale out and back in.
  7. Create a monthly budget with a forecast alert in Terraform. Then write one unit-cost metric for a service you know, such as cost per thousand requests.
Cheat sheet

Cost awareness (FinOps) — at a glance

Main things to focus on

  • FinOps loop: inform, then optimise, then operate. Attribution comes before optimisation.
  • Every resource carries owner tags, applied by default in Terraform and enforced in CI.
  • Track unit cost, such as cost per order, not just total spend.
  • The big four on a bill: compute, storage, data transfer, managed services including observability.
  • Data transfer out, across zones and through NAT gateways is the classic hidden cost.
  • In Kubernetes you pay for requests, not usage. Right-size requests, then autoscale.
  • Right-size first, then buy commitments for the baseline; use spot for interruptible work.
  • Budgets with forecast alerts and anomaly detection, so that surprises arrive mid-month and not on the invoice.

Formulas

unit cost = service cost / business unitsCost per order, per customer, per 1,000 requests
allocation rate = attributed spend / total spendHow much of the bill has an owner
CPU waste = requested cores - used coresReserved but idle capacity in Kubernetes
utilisation = used / provisionedLow sustained utilisation means a smaller size will do
commitment coverage = committed usage / steady baselineCover the floor, not the peaks
off-hours saving ~ 1 - (working hours / 168)Share of a week a sleeping dev environment is not billed

Mandatory tags

teamOwner who answers questions about it
serviceApplication it belongs to
environmentprod, staging or dev
cost-centerBudget that pays
managed-byterraform, helm or manual: where to change it
default_tags { tags = {...} }Terraform AWS provider: tag everything it creates

Finding cost and waste

aws ce get-cost-and-usage --group-by Type=DIMENSION,Key=SERVICESpend by service
aws ce get-cost-and-usage --group-by Type=TAG,Key=teamSpend by tag
aws ec2 describe-volumes --filters Name=status,Values=availableUnattached disks still being billed
kubectl top pods --containersActual CPU and memory per container
kubectl describe node NAMEAllocated requests versus node capacity
kube_pod_container_resource_requestskube-state-metrics series for requested resources

Levers

right-sizeMatch instance size and pod requests to observed peak plus headroom
autoscaleHPA for replicas, cluster autoscaler or Karpenter for nodes
scheduleScale non-production to zero outside working hours
lifecycle rulesArchive or expire old objects, snapshots and logs
commitmentsDiscount for the steady baseline, after right-sizing
spotDeep discount for stateless, interruptible work
private endpointsAvoid NAT charges for traffic to provider services

Common pitfalls

  • Optimising before attributing, so that savings are temporary because nobody owns the number.
  • Buying multi-year commitments for a fleet that was never right-sized.
  • Setting Kubernetes requests by guesswork and paying for nodes that are reserved but idle.
  • Ignoring data transfer and NAT charges when designing how services talk to each other.
  • Keeping every log at debug level, with long retention, in the most expensive storage tier.
  • Cutting redundancy to save money and discovering the real price during an outage.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →