ideas.
August 10, 2026 3 min read clidev-toolsfinancedata

Map your cloud bill back to your Terraform code

A CLI that joins your AWS cost report to Terraform state so every line item points at the module, file, and commit that created it.

The idea

A command-line tool that ingests your AWS Cost and Usage Report, reads your Terraform state files, and produces a cost breakdown keyed by module path instead of by resource ARN. Run tfcost report --month 2026-07 and get a tree: modules/data-platform cost $4,120 last month, of which aws_msk_cluster.events was $2,900. It also flags the inverse — resources billing money that no longer appear in any state file, which is where orphaned infrastructure hides.

Why build this

Every cloud cost tool answers "what did we spend?" at the resource level. Nobody on the team thinks in resource IDs. They think in the pull request that added the Kafka cluster, or the module some other team copy-pasted three times. The gap between the billing console and the code is where cost reviews die: someone exports a CSV, spends an afternoon guessing which service each line belongs to, and the exercise never repeats.

Terraform state already contains the join key. Each resource in state has a provider-assigned ID and an address like module.data_platform.aws_msk_cluster.events, and the billing data has the same IDs in its resource column. The join is mechanical — it just hasn't been packaged. Meanwhile FinOps tooling has consolidated at the enterprise end (CloudZero, Vantage) with per-seat pricing that a ten-person team won't clear procurement for, and the open-source layer stopped at Infracost, which estimates cost before apply rather than reconciling what actually got billed.

Stack sketch

  • Go, single static binary, Cobra for the CLI. Distribution via Homebrew and a GitHub Action.
  • DuckDB embedded as the query engine. CUR files land in S3 as partitioned Parquet, which DuckDB reads directly over httpfs — no warehouse, no ETL job, and a month of line items joins in seconds on a laptop.
  • Terraform state read through the terraform show -json output format, or pulled straight from an S3/Terraform Cloud backend. Parse the resource graph with hashicorp/terraform-json.
  • Blame layer: git log -L against the file that declares each resource, to attribute a module's cost to the last commit and author that touched it.
  • Output as a table, JSON, or a static HTML report. No server.

Scope for v1

In:

  • AWS only, CUR 2.0 in Parquet.
  • Local or S3-backed Terraform state, single workspace.
  • Cost rolled up by module path, resource address, and tag.
  • Untracked-resource detection: billed IDs absent from state.
  • Month-over-month diff between two report runs.

Out: GCP and Azure, Terragrunt and Terraform Cloud multi-workspace fan-out, forecasting, savings recommendations, anything with a web UI or a login. The tool prints a report and exits.

Where it could go

The natural second step is CI. A GitHub Action that comments on pull requests with "this branch adds module.search, which billed $X last month in staging" turns a monthly review into a per-change signal, and it reuses the same join with the plan file standing in for state. From there, budget assertions: a tfcost.yaml declaring per-module ceilings, with a non-zero exit when a module drifts past its limit — cost as a check that fails, not a dashboard nobody opens.

The other path is multi-cloud. GCP's billing export and Azure's cost exports both land in queryable storage with resource IDs, so the DuckDB layer generalizes; the work is in normalizing three different resource-ID formats onto Terraform addresses. That's the point where the project becomes worth a hosted version for teams that don't want the CUR pipeline in their own account.

Watch out for

The join is only as good as your state hygiene — resources created by console clicks, autoscaling, or a nested module using count will show up unattributed, and if that bucket is 40% of the bill the report loses credibility fast. Make the unattributed line a first-class part of the output rather than a footnote, and be honest that CUR data lags actual spend by up to 24 hours.