The 2026 Cloud Cost Optimization Playbook
This walks through a cloud cost audit sequence that treats cost as an architecture problem rather than a billing problem: baselining spend, rightsizing compute, committing to discounts, tiering storage, cleaning up orphaned resources, and enforcing the savings with GitOps.
1. Establish a cost baseline you can actually trust
Before touching a single resource, tag everything and build a baseline that's broken down by:
- Team / service: who owns the spend
- Environment: production vs. staging vs. dev
- Resource type: compute, storage, network egress, managed services
If your billing export isn't queryable in BigQuery (GCP) or Athena/Cost Explorer (AWS) with tag-level granularity, stop here first. You cannot optimize what you cannot attribute.
On GCP, the standard billing export lands in a table named after your billing account ID. Query net cost. The cost column alone ignores credits (sustained-use discounts, resource-based committed-use discounts, promotions, free tier), so it overstates what you pay. I also keep unlabeled spend as its own row, because that number is usually the first finding:
-- GCP: net cost per team for one invoice month, unlabeled spend included
SELECT
IFNULL((SELECT value FROM UNNEST(labels) WHERE key = 'team'), '(unlabeled)') AS team,
ROUND(
SUM(cost) + SUM(IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)),
2
) AS net_cost
FROM `my-billing-project.billing_export.gcp_billing_export_v1_XXXXXX_XXXXXX_XXXXXX`
WHERE invoice.month = '202608'
GROUP BY team
ORDER BY net_cost DESCOn AWS, the quickest equivalent is Cost Explorer from the CLI. The team tag has to be activated as a cost allocation tag in the Billing console first, and it only applies to spend from activation onwards:
aws ce get-cost-and-usage \
--time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY \
--metrics NetUnblendedCost \
--group-by Type=TAG,Key=team \
--query 'ResultsByTime[0].Groups[].[Keys[0], Metrics.NetUnblendedCost.Amount]' \
--output tableRows with the key team$ (empty value) are your unattributed spend. Swap the group-by for Type=DIMENSION,Key=SERVICE to get the per-service view. For anything beyond a monthly snapshot, set up the Cost and Usage Report (CUR) export and query it in Athena.
2. Rightsize compute, but verify with real utilization data
Rightsizing is the single highest-leverage lever, and the highest-risk one if done blindly. The process that works:
- Pull 30-90 days of CPU, memory, and network utilization per instance/node pool.
- Flag anything running well below its allocation (I start with under 20% average CPU and check memory separately) as a rightsizing candidate.
- Cross-reference against peak windows (month-end batch jobs, Black Friday traffic) before downsizing.
- Roll out changes behind autoscaling policies defined in Terraform, never as manual console edits.
3. Commit to what you know you'll use
Commit only after rightsizing, otherwise you lock in a discount on waste. Committed Use Discounts (GCP) or Savings Plans (AWS) should cover the steady floor you see across the whole 30-90 day window, which sits below both the average and the peak. Anything above that floor stays on-demand or autoscaled. If the baseline is still moving, start with a shorter term or a partial commitment. Both clouds show commitment utilization, so check it monthly and only top up once the existing commitments are fully used.
4. Tier storage and cut network waste
Storage and network savings come from configuration changes rather than commitments, so they fit well in Terraform.
Lifecycle rules move cold data to cheaper classes automatically. On GCS:
resource "google_storage_bucket" "app_logs" {
name = "acme-app-logs"
location = "EU"
uniform_bucket_level_access = true
lifecycle_rule {
condition {
age = 30
}
action {
type = "SetStorageClass"
storage_class = "NEARLINE"
}
}
lifecycle_rule {
condition {
age = 365
}
action {
type = "Delete"
}
}
}The AWS equivalent is aws_s3_bucket_lifecycle_configuration with transitions to STANDARD_IA or a Glacier class. Before tiering, check the minimum storage durations and per-GB retrieval fees of the colder classes, because data that's read often can end up costing more there. Very small objects rarely benefit either.
On the network side, look for cross-region and cross-AZ traffic. Teams tend to forget to audit this line item, and it grows fast. Filter the billing export on egress SKUs (GCP) or the DataTransfer usage types (AWS) to see where it's coming from.
5. Kill the "temporary" resources that never got cleaned up
Every audit turns up orphaned disks, idle load balancers, forgotten dev clusters and unattached IPs. I codify a weekly automated sweep as a Terraform-managed Cloud Function or Lambda. It flags anything unlabeled or idle past a threshold, and it never deletes anything automatically.
6. Make the savings stick with GitOps
The savings that don't survive the next quarter are the ones enforced by a person remembering to check a dashboard. The ones that stick are enforced by policy:
- All infrastructure changes go through Terraform + pull request review (no console clicking).
- Budget alerts and anomaly detection are defined as code.
- A CI check blocks cost increases above an agreed ceiling.
Guardrail 1: a cost check on every pull request
Infracost estimates the monthly cost delta of a Terraform change and comments it on the PR. This workflow also fails when the increase exceeds a threshold, unless a reviewer adds a cost-approved label. Make the job a required status check in branch protection so the ceiling can't be skipped:
# .github/workflows/infracost.yml
name: infracost
on:
pull_request:
paths: ["infra/**"]
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
pull-requests: write
jobs:
cost:
runs-on: ubuntu-latest
env:
TF_ROOT: infra
MAX_MONTHLY_INCREASE: "500" # USD per month
steps:
- uses: infracost/actions/setup@v4
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Checkout base branch
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Estimate base branch cost
run: |
infracost breakdown --path="$TF_ROOT" \
--format=json --out-file=/tmp/infracost-base.json
- name: Checkout PR branch
uses: actions/checkout@v7
- name: Diff PR against base
run: |
infracost diff --path="$TF_ROOT" \
--format=json \
--compare-to=/tmp/infracost-base.json \
--out-file=/tmp/infracost.json
- name: Comment on the PR
run: |
infracost comment github --path=/tmp/infracost.json \
--repo="$GITHUB_REPOSITORY" \
--github-token="${{ github.token }}" \
--pull-request="${{ github.event.pull_request.number }}" \
--behavior=update
- name: Enforce the monthly increase ceiling
if: ${{ !contains(github.event.pull_request.labels.*.name, 'cost-approved') }}
run: |
delta=$(jq -r '.diffTotalMonthlyCost // "0"' /tmp/infracost.json)
echo "Monthly cost delta: ${delta} USD"
if awk -v d="$delta" -v max="$MAX_MONTHLY_INCREASE" 'BEGIN { exit !(d > max) }'; then
echo "::error::Monthly cost increase of ${delta} USD exceeds ${MAX_MONTHLY_INCREASE} USD. Add the 'cost-approved' label after review."
exit 1
fiThere are two caveats. Infracost prices resources from the Terraform code, so usage-based costs (egress, requests, log volume) are estimated as zero unless you give it a usage file. And PRs from forks don't get repository secrets, so this suits internal repos. If you'd rather not maintain the workflow, Infracost's GitHub App or its newer diff action handle the commenting with less setup. The threshold then lives in Infracost's own guardrail settings instead of your repo.
Guardrail 2: budget alerts in Terraform
The PR check only sees changes you make in code. Budgets cover spend that drifts without one, such as traffic growth, a runaway job or a forgotten test cluster. Define them next to the infrastructure they watch:
# AWS: alert at 80% of actual spend and when the forecast crosses 100%
resource "aws_budgets_budget" "monthly_total" {
name = "monthly-total"
budget_type = "COST"
limit_amount = "5000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["finops@example.com"]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["finops@example.com"]
}
}
# GCP: same idea, scoped to the production project
resource "google_billing_budget" "prod_monthly" {
billing_account = var.billing_account_id
display_name = "prod-monthly"
budget_filter {
projects = ["projects/${var.prod_project_number}"]
}
amount {
specified_amount {
currency_code = "USD"
units = "5000"
}
}
threshold_rules {
threshold_percent = 0.8
}
threshold_rules {
threshold_percent = 1.0
spend_basis = "FORECASTED_SPEND"
}
all_updates_rule {
monitoring_notification_channels = [var.finops_notification_channel_id]
disable_default_iam_recipients = true
}
}Budgets send alerts but don't cap spend. Route the notifications to a channel someone reads, and handle a breached budget like any other incident by finding the change and fixing it in code. On AWS, aws_ce_anomaly_monitor and aws_ce_anomaly_subscription add anomaly detection the same way.
The result
In the audits I've run, the biggest savings, and the ones that last, came from attribution, rightsizing before committing, and the guardrails above. How much you save depends heavily on how much waste there is to begin with, so measure it against your own baseline from step 1 rather than an industry number. Reliability holds up because every change goes through the same review process as a feature deploy.
If you want this audit run against your own environment, that's what my Cloud Cost Audit engagement covers. You get a cost breakdown report, a prioritized savings roadmap, and the Terraform PRs to implement it.
Want to actually run this in production?
This tutorial covers the concepts and architecture. If you want to implement it in your own infrastructure, or get good enough to own this problem long-term, I offer 1:1 mentoring built around your real environment, not a generic course.
This tutorial
- Core architecture & key concepts
- Illustrative code snippets
- The reasoning behind each decision
1:1 mentoring
- Working sessions on your own environment
- Direct answers to the edge cases you're hitting
- Feedback on your actual implementation
- Ongoing support as you build it out
Related Tutorials
Kubernetes FinOps on EKS with Karpenter
Replace static EKS node groups with Karpenter's just-in-time, bin-packing provisioning to cut compute spend.
Provisioning a Secure AWS VPC with Terraform
A production-shaped AWS VPC in Terraform, with public and private subnets, NAT, route tables and safer defaults.
Kubernetes Cost Optimization on GKE
Rightsize requests with VPA, tune HPA, add Spot node pools and get the cluster autoscaler to scale back down.