038 Project 38: Terraform Cost Estimator
038 Build a Terraform Cost Estimator (Simple)
Estimate monthly cost delta from a Terraform plan JSON by mapping resource types to rough unit prices. Not a substitute for Infracost—but excellent for learning plan analysis and CI annotations.
plan.json -> actions × unit price table -> monthly delta $
Problem statement
tf-cost [-prices prices.json] <plan.json>
createadds unit monthly costdeletesubtracts unit monthly costreplacetreat as delete+create (net depends on type change; base: same type → ~0)- Unknown types contribute 0 (list them as warnings)
Acceptance criteria
- Parses plan JSON
resource_changes - Prints estimated monthly delta
- Warns on unknown types that change
- Exit 2 on parse errors; 0 on success
- Prices overridable via JSON file
Setup
mkdir tfcost && cd tfcost
go mod init example.com/tfcost
# go 1.27prices.json example:
{
"aws_instance": 15.0,
"aws_db_instance": 120.0,
"aws_lb": 25.0
}Full main.go
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
)
type Plan struct {
ResourceChanges []struct {
Address string `json:"address"`
Type string `json:"type"`
Change struct {
Actions []string `json:"actions"`
} `json:"change"`
} `json:"resource_changes"`
}
func loadPrices(path string) (map[string]float64, error) {
defaults := map[string]float64{
"aws_instance": 15.0,
"aws_db_instance": 120.0,
"aws_lb": 25.0,
}
if path == "" {
return defaults, nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var m map[string]float64
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
for k, v := range defaults {
if _, ok := m[k]; !ok {
m[k] = v
}
}
return m, nil
}
func estimate(p Plan, cost map[string]float64) (delta float64, unknown []string) {
seenUnknown := map[string]bool{}
for _, rc := range p.ResourceChanges {
unit, ok := cost[rc.Type]
if !ok {
if !seenUnknown[rc.Type] && len(rc.Change.Actions) > 0 && rc.Change.Actions[0] != "no-op" {
// track types that actually change
for _, a := range rc.Change.Actions {
if a == "create" || a == "delete" {
seenUnknown[rc.Type] = true
unknown = append(unknown, rc.Type)
break
}
}
}
continue
}
actions := rc.Change.Actions
if len(actions) == 2 && actions[0] == "delete" && actions[1] == "create" {
// same type replace → approx net 0 for this toy model
continue
}
for _, a := range actions {
switch a {
case "create":
delta += unit
fmt.Printf("+ $%.2f %s (%s)\n", unit, rc.Address, rc.Type)
case "delete":
delta -= unit
fmt.Printf("- $%.2f %s (%s)\n", unit, rc.Address, rc.Type)
}
}
}
return delta, unknown
}
func main() {
pricesPath := flag.String("prices", "", "optional prices JSON")
flag.Parse()
if flag.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: tf-cost [-prices prices.json] <plan.json>")
os.Exit(2)
}
cost, err := loadPrices(*pricesPath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
b, err := os.ReadFile(flag.Arg(0))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
var p Plan
if err := json.Unmarshal(b, &p); err != nil {
fmt.Fprintln(os.Stderr, "json:", err)
os.Exit(2)
}
delta, unknown := estimate(p, cost)
for _, u := range unknown {
fmt.Fprintf(os.Stderr, "warn: no price for type %s\n", u)
}
fmt.Printf("estimated monthly delta: $%.2f\n", delta)
}Run and verification
go run . plan.json
go run . -prices prices.json plan.jsonTests
package main
import "testing"
func TestEstimateCreate(t *testing.T) {
var p Plan
rc := p.ResourceChanges
_ = rc
item := struct {
Address string `json:"address"`
Type string `json:"type"`
Change struct {
Actions []string `json:"actions"`
} `json:"change"`
}{Address: "aws_instance.web", Type: "aws_instance"}
item.Change.Actions = []string{"create"}
p.ResourceChanges = append(p.ResourceChanges, item)
delta, _ := estimate(p, map[string]float64{"aws_instance": 15})
if delta != 15 {
t.Fatalf("%v", delta)
}
}Stretch goals
- Regional price multipliers.
- Count module expansion addresses.
- Fail CI if delta > budget (
-max-delta 50). - Integrate with risk reporter output.
Pitfalls
| Pitfall | Fix |
|---|---|
| Treating prices as accurate | label as rough estimate |
| Ignoring replace | define policy (net 0 vs full) |
| Double-counting | classify actions carefully |
Learning goals
- Plan-driven financial signals
- Extensible price tables
- Honest limits of toy cost models