TerraTally is a post-apply FinOps tool that attributes actual cloud billing costs back to Terraform resources, modules, workspaces, repositories, pull requests, and owners.
It joins Terraform state identities, apply events, Git metadata, and billing exports into a cost ledger. The default storage is a local JSON ledger, with PostgreSQL schema and SQL export support for production use.
- Terraform
state pullandterraform show -jsonidentity extraction. - Secret-safe state handling through allowlisted identity/tag fields.
- AWS CUR CSV import, Athena query generation, and Athena query-result import.
- Azure Cost Management CSV and Google Cloud Billing CSV import.
- Direct ARN/ID/bucket/name matching, Terraform tag matching, ambiguous split, shared-cost allocation, and unmatched buckets.
- Attribution confidence scoring.
- Apply event ingestion from CLI, Terraform plan JSON, GitHub Actions, Atlantis, and HCP Terraform-style payloads.
- Ownership resolution from tags, config rules, CODEOWNERS, PR author mapping, git last author, workspace defaults, and
unknown. - JSON, CSV, and Markdown reports for resources, modules, owners, unmatched costs, tag remediation, metrics, and PR deltas.
- REST API and a compact dashboard at
/dashboard. - GitHub PR comment and Slack webhook notification adapters with dry-run support.
- PostgreSQL schema, views, and SQL export/push support.
- Go 1.26 or newer.
- Optional: AWS CLI for live Athena imports.
- Optional:
psqlfor pushing exported SQL to PostgreSQL. - Optional:
GITHUB_TOKEN,SLACK_WEBHOOK_URL,DATABASE_URL, andTERRATALLY_API_TOKENfor integrations.
The implementation uses only the Go standard library.
Run tests:
go test ./...Ingest Terraform state:
go run ./cmd/terratally ingest-state \
--workspace-id prod-network \
--repository acme/infra \
--terraform-root envs/prod/network \
--state-json state.json \
--account-id 123456789012 \
--region ap-northeast-1Record an apply event:
go run ./cmd/terratally record-apply \
--workspace-id prod-network \
--repository acme/infra \
--commit-sha abc123 \
--status success \
--pr-number 123 \
--plan-json tfplan.jsonImport AWS CUR aggregate CSV and attribute costs:
go run ./cmd/terratally import-aws-cur \
--csv cur.csv \
--from 2026-05-01 \
--to 2026-06-01
go run ./cmd/terratally attribute \
--from 2026-05-01 \
--to 2026-06-01Generate a report:
go run ./cmd/terratally report resources \
--from 2026-05-01 \
--to 2026-06-01 \
--format markdownBuild the CLI:
go build -o ./bin/terratally ./cmd/terratallyCommon commands:
go run ./cmd/terratally checklist aws-cur --format markdown
go run ./cmd/terratally import-aws-cur --athena-database cur_db --athena-table cur_table --from 2026-05-01 --to 2026-06-01 --dry-run
go run ./cmd/terratally report unmatched --from 2026-05-01 --to 2026-06-01 --format csv
go run ./cmd/terratally serve --host 127.0.0.1 --port 8787Default paths:
- Ledger:
.terratally/ledger.json - Config:
.terratally.yaml
Use --ledger and --config to override them.
Start from .terratally.example.yaml. It defines workspaces, AWS Athena/CUR settings, ownership rules, tag aliases, redaction, attribution behavior, and report defaults.
Ownership precedence:
- Terraform tags:
Owner,Team,Service,Environment, or configured aliases. .terratally.yamlownership rules.- CODEOWNERS.
- PR author team mapping.
- Git last author, when
ownership.git_blameis enabled. - Workspace default owner.
unknown.
import-aws-cur accepts Athena-style aggregate CSV:
usage_date,account_id,region,product_code,usage_type,operation,resource_id,tag_owner,tag_service,unblended_cost
2026-05-11,123456789012,ap-northeast-1,AmazonEC2,BoxUsage,RunInstances,i-abc,team-platform,checkout,12.34Athena SQL can be generated with:
go run ./cmd/terratally import-aws-cur \
--athena-database cur_db \
--athena-table cur_table \
--from 2026-05-01 \
--to 2026-06-01 \
--dry-runStart the API:
go run ./cmd/terratally serve --host 127.0.0.1 --port 8787Dashboard:
http://127.0.0.1:8787/dashboard
Main endpoints:
GET /api/v1/costs/resourcesGET /api/v1/costs/modulesGET /api/v1/costs/ownersGET /api/v1/costs/prs/{owner}/{repo}/{pr_number}GET /api/v1/attribution/unmatchedGET /api/v1/attribution/tag-remediationGET /api/v1/metricsPOST /api/v1/apply-eventsPOST /api/v1/state-snapshotsPOST /api/v1/imports/aws-curPOST /api/v1/imports/azure-costPOST /api/v1/imports/gcp-billingPOST /api/v1/attribute
Set TERRATALLY_API_TOKEN or pass --api-token to require bearer auth for API routes.
Schema and views:
Export the local ledger as SQL:
go run ./cmd/terratally export-sql --output ledger.sqlPush to PostgreSQL:
DATABASE_URL=postgres://user:pass@host:5432/db \
go run ./cmd/terratally push-postgres- Raw Terraform state is not persisted.
- Only identity/tag-related allowlisted attributes are stored.
- Configure tag redaction for sensitive tag keys.
- Do not run state or billing ingestion from untrusted external pull requests.
- Use least-privilege AWS, GitHub, Slack, and database credentials.
go test ./...
go vet ./...
go run ./cmd/terratally --helpMore examples are in docs/quickstart.md.