📋 In This Post
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.
| Difficulty | Advanced — one step is console-only, and one error message names nothing at all |
|---|---|
| Deployment | Terraform via HCP Terraform, plus one form a human must click |
| Reproduce it | Lab 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
| Service | Purpose here | What it is |
|---|---|---|
| Cloud Billing export | Puts the bill somewhere it can be queried | Writes usage and cost rows to BigQuery continuously, so spend becomes a table you can query rather than a dashboard you read. |
| BigQuery | Holds the exported cost and pricing data | Holds the export. Cost questions become SQL. |
| Cloud Billing budgets | Raises the alert this week makes programmatic | Watches spend against a threshold. Alerts; does not cap. |
| Pub/Sub | Carries the budget notification to something that can act | The budget's programmatic notification path, so an alert can reach automation instead of only an inbox. |
| Organization Policy | Relaxed, narrowly, so the budget can publish at all | A 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
The flow
- Check what already exists. In this organization that answered the week before it started: an export had been running for three months.
- Create a BigQuery dataset for pricing data and a Pub/Sub topic for budget events.
- Write a project-scoped exception to Week 03's domain restricted sharing constraint, and wait for it to propagate.
- Attach the topic to the budget, which makes Cloud Billing write its own service agent onto the topic as publisher.
- Configure the export link itself in the console — there is no API for it.
- 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.policyAdminat the organization, for the scoped exception.roles/billing.costsManagerto manage budgets, androles/billing.viewerto configure an export —billing.accounts.getPricinglives only inviewerandadmin.- 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.
Step 1 — Ask what already exists
Why: because the week's premise might be wrong. Here it was.
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.
Step 3 — Attach the topic to the budget
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
| Test | Expected | Actual | Status |
|---|---|---|---|
| Infrastructure deployed | 7 resources | 7 added, 0 changed, 0 destroyed | Pass |
| An export exists with rows | rows > 0 | 2,294 rows, 4 projects, since 2026-07-01 | Pass |
| It answers the cost question | a real total | $0.006 for the whole lab to date | Pass |
| Budget has a programmatic path | all_updates_rule set | set, topic attached | Pass |
| Publisher identity | read back, not guessed | billing-budget-alert@system.gserviceaccount.com | Pass |
| Exception blast radius | one project | project-scoped; organization untouched | Pass |
| A budget threshold actually publishes a message | message observed on the topic | not tested — spend never approached the threshold | Unverified |
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
| Control | Decision and trade-off |
|---|---|
| Scoped exception | The 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 privilege | The operator holds costsManager, which covers viewing spend and managing budgets and nothing else — admin would also allow detaching projects from billing. |
| Out-of-band grants | Billing-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 exposure | The 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
- 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.
- 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.
- 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.
- Never write a service agent address from memory. Read it back from the IAM policy — an earlier draft guessed one that does not exist.
- Budgets alert, they do not cap, and the spend caps that now exist cover four services this lab does not run.
References
- Lab guide — permission split, the exception, the console-only step
- Export billing data to BigQuery
- Budgets and alerts
- Programmatic budget notifications
- Source repository
Comments