Home› Blog› Week 6 — Billing Export on Google Cloud: The Data …
GCP Weekly Lab GCP Terraform

Billing export and budget alerting

Make cost a queryable fact rather than a monthly surprise — and find out the export had been running for three months already.

Verified against current vendor documentation on 27 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
GCP Platform Engineering Lab · Week 6 of 58

Lab Overview

The problem. Five weeks of this lab claimed to cost approximately nothing, and not one of those claims could be checked. A cost figure you cannot query is an assertion.

Objective. Make spend queryable in BigQuery, and give the existing budget a programmatic notification path so an alert can reach something other than an inbox.

Expected outcome. A SQL-answerable cost history, a budget that publishes to Pub/Sub, and a measured total for every prior week.

What actually happened first: the export was already running. A detailed usage cost export had been landing in the seed project since 1 July, unqueried, for three months. The week's premise was false before it was written.

DifficultyAdvanced — one step is console-only, and one error message names nothing at all
DeploymentTerraform via HCP Terraform, plus one form a human must click
Reproduce itLab guide — the permission split, the exception, the console step
Cost$0 for what this week adds — and for the first time, a measured claim

Cloud Services and Concepts

ServicePurpose hereWhat it is
Cloud Billing exportPuts the bill somewhere it can be queriedWrites usage and cost rows to BigQuery continuously, so spend becomes a table you can query rather than a dashboard you read.
BigQueryHolds the exported cost and pricing dataHolds the export. Cost questions become SQL.
Cloud Billing budgetsRaises the alert this week makes programmaticWatches spend against a threshold. Alerts; does not cap.
Pub/SubCarries the budget notification to something that can actThe budget's programmatic notification path, so an alert can reach automation instead of only an inbox.
Organization PolicyRelaxed, narrowly, so the budget can publish at allA project-scoped exception to the domain restriction from Week 03 — without it the budget cannot be attached to the topic at all.

Two concepts to carry into the rest. First, billing is orthogonal to the resource hierarchy: a billing account is not a parent of anything, so cost separation comes from labels and per-project budgets rather than from folders. Second, a budget alerts, it does not cap — spending continues past the threshold. Spend caps do now exist (Preview, July 2026) but cover only Gemini API, Agent Platform, Cloud Run and Cloud Run functions, none of which this lab runs.

Architecture and Workflow

Cost as a table you can query GCP Lab · Week 6 billing account the thing that pays Icon_24px_BigQuery_Color BigQuery usage rows, asked in SQL a budget watches spend publishes thresholds Icon_24px_Pub-Sub_Color Pub/Sub threshold events exports to notifies attaching the topic needed a project-scoped exception to domain restricted sharing jayanthkatta.com budget:96,520,380,160

The flow

  1. Check what already exists. In this organization that answered the week before it started: an export had been running for three months.
  2. Create a BigQuery dataset for pricing data and a Pub/Sub topic for budget events.
  3. Write a project-scoped exception to Week 03's domain restricted sharing constraint, and wait for it to propagate.
  4. Attach the topic to the budget, which makes Cloud Billing write its own service agent onto the topic as publisher.
  5. Configure the export link itself in the console — there is no API for it.
  6. Query the export and produce a measured cost figure for every prior week.

Two decisions worth naming

The exception is project-scoped, not organization-wide. Relaxing a security control everywhere to make one budget work would trade a real guardrail for a notification. The override names one project, and the lab guide says to review it.

The pre-existing export was left where it was. Re-pointing an export strands its history — the old table stops receiving rows and the new dataset backfills at most a month. Consolidating would have destroyed three months of the only cost data the lab had.

Prerequisites

  • A billing account, and someone with roles/billing.admin — the Workspace account cannot grant billing-account roles at all.
  • roles/orgpolicy.policyAdmin at the organization, for the scoped exception.
  • roles/billing.costsManager to manage budgets, and roles/billing.viewer to configure an export — billing.accounts.getPricing lives only in viewer and admin.
  • Week 03's domain restriction in place, or the exception has nothing to except.
  • Terraform 1.9+ with hashicorp/google ~> 6.0.

The exact permission split, and the REST workaround for a broken gcloud command, are in the lab guide.

Implementation

Seven resources and one console step. Commands are in the lab guide.

Repository structure: LAB.md, terraform, scripts and docs, each with a one-line responsibility.
How the week is organised. Full source on GitHub.

Step 1 — Ask what already exists

Why: because the week's premise might be wrong. Here it was.

BigQuery showing a billing export dataset that had been receiving rows since July.
The finding that reframed the week: a detailed usage cost export running since 1 July, unqueried — 2,294 rows across 4 projects. Nothing had to be built to answer the cost question; it had to be asked.

Step 2 — Scope an exception, then wait

Why: attaching a topic to a budget makes Cloud Billing write its own service agent onto the topic. Week 03's iam.allowedPolicyMemberDomains refuses that, and the API says only Error 400: Precondition check failed.

Action: a project-scoped override plus a 120-second time_sleep, because org policy changes take 75–108 seconds to take effect in either direction.

Organization policy page showing the domain restricted sharing override applied at a single project.
Validates the blast radius: the exception names one project. The organization-level constraint is untouched — which is the difference between an exception and a hole.

Step 3 — Attach the topic to the budget

Budget configuration showing the Pub/Sub topic attached for programmatic notifications.
Validates Step 3: the budget now publishes threshold events. Read the publisher identity back from the topic's IAM policy — it is billing-budget-alert@system.gserviceaccount.com, singular "budget", and an earlier draft guessed an address that does not exist.

Step 4 — Configure the export link, by hand

Why by hand: there is no Terraform resource, and the form cannot be automated. Playwright sets project and dataset correctly and the button transitions, but no save request is ever issued. Treat it as human-only.

Validation and Evidence

TestExpectedActualStatus
Infrastructure deployed7 resources7 added, 0 changed, 0 destroyedPass
An export exists with rowsrows > 02,294 rows, 4 projects, since 2026-07-01Pass
It answers the cost questiona real total$0.006 for the whole lab to datePass
Budget has a programmatic pathall_updates_rule setset, topic attachedPass
Publisher identityread back, not guessedbilling-budget-alert@system.gserviceaccount.comPass
Exception blast radiusone projectproject-scoped; organization untouchedPass
A budget threshold actually publishes a messagemessage observed on the topicnot tested — spend never approached the thresholdUnverified

The headline result is row three. $0.006 across four months is the first measured cost figure in this lab, and it retroactively makes every previous week's "$0" claim checkable instead of asserted. Row seven is the honest gap: the notification path is configured and the publisher binding exists, but no threshold was ever crossed, so no message has been observed end to end.

Security and Cost

Security decisions

ControlDecision and trade-off
Scoped exceptionThe domain restriction is relaxed for one project, not the organization. It is still a deliberate hole in a security control and the guide says to review it.
Least privilegeThe operator holds costsManager, which covers viewing spend and managing budgets and nothing else — admin would also allow detaching projects from billing.
Out-of-band grantsBilling-account roles cannot be granted by the Workspace identity at all, so one grant is made by a different identity, by hand, and recorded.
Data exposureThe export contains per-project spend and resource names. It lives in a project-scoped dataset and is not shared.

Cost

$0 for what this week adds. The export is free, the budget is free, and the BigQuery storage is kilobytes. $0.006 total for the lab since 1 July — measured from the export itself, which is the whole point of the week.

Budgets alert; they do not cap. Spend caps shipped in Preview on 27 July 2026 and genuinely pause services, but cover only Gemini API, Gemini Enterprise Agent Platform, Cloud Run and Cloud Run functions. Compute Engine, Cloud Storage and BigQuery are not eligible, so for this lab the accurate sentence is "GCP can now cap, on four services, none of which we run".

Troubleshooting and Lessons

Four words and no resource

Problem: attaching the topic to the budget failed with Error 400: Precondition check failed. No resource, no policy, no details array.

Root cause: Week 03's domain restricted sharing constraint, refusing Cloud Billing's own Google-owned service agent.

Resolution: diagnosed by elimination, not by re-reading documentation — a bogus topic gave NOT_FOUND so resolution worked; two different budgets failed identically so it was not the budget; testIamPermissions confirmed the caller held setIamPolicy. Identical failure as the pipeline identity and as a human organization admin pointed at an organization-level cause.

Lesson: when an error names nothing, stop reading and start eliminating. The console, incidentally, stated the real cause in full on the budget's own page — the API did not.

The week's premise was already false

Problem: the lab was written to solve "there is no queryable cost source". There had been one for three months.

Resolution: keep it, query it, and report the real number.

Lesson: read live state before believing a week's own premise, not just its configuration. The config was accurate; the premise was not.

A form that cannot be automated

Problem: the export link has no Terraform resource, and browser automation sets every field correctly without ever issuing a save.

Lesson: some surfaces are human-only. Recognising one early is cheaper than proving it for an hour.

Cleanup and Key Takeaways

Cleanup

This week is exempt from teardown, deliberately. scripts/cleanup.sh does not destroy the export or the budget. A cost guardrail that exists only during the week it was built is not a guardrail — and every later week's $0 claim is provable only because this export keeps running.

If you do tear it down, remove the budget first and the policy exception last: the exception is what permits the budget's notification binding to exist. Commands are in the lab guide.

Ongoing charges: effectively none — kilobytes of BigQuery storage.

Key takeaways

  1. Read live state before trusting a week's premise. The export had been running for three months. The configuration was right; the assumption was wrong.
  2. An opaque error is diagnosed by elimination. "Precondition check failed" names nothing — a bogus resource, a second budget and a permissions probe named it between them.
  3. Google's services write as themselves. A budget's notification path requires Cloud Billing's own service agent on your topic, which a domain restriction will refuse.
  4. Never write a service agent address from memory. Read it back from the IAM policy — an earlier draft guessed one that does not exist.
  5. Budgets alert, they do not cap, and the spend caps that now exist cover four services this lab does not run.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent