Cost engine¶
All cost arithmetic lives in src/cost/. The rules are adapted from the AWS billing-and-cost-management skill: never reason about money, run code; public on-demand list rates only; 730 hours per month; never invent a price.
| File | Role |
|---|---|
pricefile.mjs |
Ingests Cloud Billing Catalog SKUs into compact rate rows (skuRows, forRegion). |
pricebook.mjs |
loadPriceBook(region); dim(), tryDim(), tiered(), round4(), money(), HOURS_PER_MONTH, PricingError. ARCHIFY_GCP_PRICES_DIR overrides the data folder (tests use it). |
pricers.mjs |
One pricer per product; SERVICE_PRICER (catalog id → pricer), NO_CHARGE, machineShape (machine type → vCPU and memory). |
estimate.mjs |
estimateCost(diagram, options): runs pricers, totals, sensitivity, what-ifs, provenance. |
Price book (data/prices/<region>.json)¶
{ "schema_version": "archify-gcp.prices.v1", "region": "us-central1", "retrievedAt": "…",
"services": { "Compute Engine": { "serviceId": "6F81-5844-456A", "publicationDate": "…",
"rows": [ { "sku": "<skuId>", "d": "E2 Instance Core running in Americas", "g": "CPU", "f": "Compute", "ut": "OnDemand",
"unit": "h", "usd": "0.021811", "b": 0, "e": null, "r": ["us-central1"] } ] } } }
d is the SKU description, g the resource group, f the resource family, ut the usage type, unit the SKU's usage unit (h, GiBy.mo, GiBy, TiBy, count, s…), b/e the tier begin/end in that unit (the API gives the start; ends are derived). Free tiers are zero-price first tiers, so tiered() includes them automatically. dim(code, predicate, what) returns the tier rows for one dimension and throws if the predicate matches SKUs with different prices (the same SKU listed for several regions is fine). A silently wrong SKU is worse than "not estimated".
Getting the data¶
The Cloud Billing Catalog API (cloudbilling.googleapis.com/v1/services and /v1/services/{id}/skus) is the official source and needs an API key (enable the Cloud Billing API on a project; create a key). scripts/fetch-prices.mjs reads GOOGLE_API_KEY, pages through each service's SKUs, keeps on-demand SKUs that apply to the region (or to a multi-region / global scope) and writes the compact rows. There is no keyless public price list of comparable quality, so the repository does not commit a price book.
Pricer patterns are unverified until the first real fetch
The SKU description patterns in pricers.mjs were written from the structure of the catalog without a live fetch. Tests run the pricers against a synthetic fixture (test/fixtures/prices/) that proves the arithmetic and the failure modes. After the first real prices:fetch, run archify-gcp cost on the examples: a pricer whose pattern does not match reports needs-input ("no price for …"); fix the pattern, add the real description to a test and re-run.
A pricer¶
run: {
label: "Cloud Run",
run({ pb, u, vol }) {
const req = vol("requestsPerMonth", 1e6), ms = u("avgDurationMs", 200), vcpu = u("vcpu", 1);
const cpu = pb.dim("Cloud Run", (r) => /CPU/i.test(r.d) && !/Idle|Min Instance/i.test(r.d), "Cloud Run CPU");
return { lines: [ line("vCPU-seconds", cpu, req * (ms / 1000) * vcpu) ] };
},
}
u(key, default)readsnode.usage[key], records it as spec or default, and returns it.vol(key, default)is the same for volumes that scale with traffic (sensitivity multiplies these; fixed capacity such as instance counts usesu).line(label, rows, qty, opts)prices through tiers and recordsqty,unit,rate,sku,usagetype. Quantities must be expressed in the SKU's own unit.- Return
{ lines, notes?, notItemized? }; throwPricingErrorfor anything that cannot be priced without guessing.estimate.mjsturns that intostatus: "error"orneeds-input. - What-ifs live in
computeWhatIfsinestimate.mjs: Cloud SQL high availability and Nearline tiering, each recomputed with the same pricer and the same price book.
Adding a product¶
- Add its Cloud Billing display name to
SERVICESinscripts/fetch-prices.mjsand fetch:GOOGLE_API_KEY=… node scripts/fetch-prices.mjs --services "<name>". - Write the pricer in
pricers.mjsand map the catalog id inSERVICE_PRICER; add aCATEGORYentry and aSERVICE_CODESentry inestimate.mjs. - Add its usage keys to the user guide table (
docs/user-guide/cost.md). - Add SKUs with the real descriptions to
test/fixtures/prices/and a test that reads the rate from the price book itself (never hard-codes a price) and checks the formula. - Verify one number by hand against the Google Cloud Pricing Calculator.
estimateCost result¶
{ schema_version, asOf, region, currency, scale, basis,
totals: { monthlyUsd, annualUsd, byCategory },
coverage: { nodes, estimated, override, noCharge, notBillable, notItemized, needsInput, notEstimated, error },
confidence: "indicative" | "usage-based", defaultedAssumptions: [ { node, key, value } ],
nodes: [ { id, label, service, status, pricer, category, monthlyUsd, lines, assumptions, notes } ],
sensitivity: [ { scale, monthlyUsd } ], whatIfs: [ … ], priceBook: { region, retrievedAt, publications } }
Totals include only estimated and override nodes. Pass asOf: new Date(…) in tests for reproducibility. analyze() catches "no price book" and returns { cost: null, costNote } so the page simply omits the Cost tab.
Gotchas¶
- A SKU's
usageUnitis not always the unit people think in (TiBy,GiBy.mo,GiBy.h,s): always read it from the row. - Several SKUs can look alike (zonal vs regional, Standard vs Enterprise editions, request-based vs instance-based billing): filter explicitly and let
dim()fail on ambiguity. - A
Set.add(...codes)only adds the first argument — loop. - The "scaled by traffic" flag must be set on the assumption entry even when the key already exists.