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.
- 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.
| Phase | Question | Activities |
|---|---|---|
| Inform | Where is the money going, and who is spending it? | Tagging, allocation, dashboards, showback to teams |
| Optimise | Where are we paying for something we do not use? | Right-sizing, deleting waste, commitments, architecture changes |
| Operate | How 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.
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.
| Tag | Example | Answers |
|---|---|---|
team | shop-platform | Who owns this and gets asked about it? |
service | orders-api | Which application is it part of? |
environment | prod, staging, dev | Can it be switched off at night? |
cost-center | cc-4100 | Which budget pays for it? |
managed-by | terraform | Where 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.
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.
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.
# 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=teamArchitecture 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.
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'sum by (namespace) (kube_pod_container_resource_requests{resource="cpu"})
-
sum by (namespace) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))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
| Model | Discount | Trade-off | Use for |
|---|---|---|---|
| On-demand | None | Full flexibility | Spiky or unpredictable load; anything new |
| Commitment (reserved, savings plans, committed use) | Substantial | You pay for one to three years whether you use it or not | The steady baseline that runs all day, every day |
| Spot / preemptible | Largest | Capacity can be reclaimed at short notice | Stateless, 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.
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.
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.