Home› Blog› Week 7 — Cloud Asset Inventory: The Rule I Wrote a…
GCP Weekly Lab GCP Terraform

Auditing a resource hierarchy with Cloud Asset Inventory

Ask the organization what it actually contains, rather than what the Terraform says it should.

Verified against current vendor documentation on 29 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 7 of 58

Lab Overview

The problem. Six weeks of Terraform describe what this organization is supposed to contain. Nothing so far asks what it does contain — including the things created by hand, created before the lab existed, or created by someone who never read the plan.

Objective. Stand up an organization-wide asset feed for change notifications and a BigQuery snapshot for point-in-time queries, then run a real audit against the result.

Expected outcome. A queryable inventory of every asset in the organization, and at least one finding the Terraform could not have told us.

DifficultyIntermediate — the deployment is straightforward; the service-agent grants are not obvious
DeploymentTerraform via HCP Terraform, plus one script, because an export is not a resource
Reproduce itLab guide — grants, snapshot, audit queries, cleanup
Cost$0 — feeds and exports are free; the snapshot is kilobytes

Cloud Services and Concepts

ServicePurpose hereWhat it is
Cloud Asset InventoryMakes the whole organization a queryable tableA history and inventory of every asset in the organization. Answers "what exists" rather than "what should exist".
Asset feedReports changes as they happen, rather than on demandThe streaming half: publishes a message whenever an asset changes. Declarative, so Terraform manages it.
Snapshot exportCaptures the organization as it stands at one momentThe point-in-time half: writes the whole inventory to BigQuery. An operation with a moment attached, so a script runs it.
Pub/SubReceives the feed's change notificationsWhere the feed delivers change notifications.
BigQueryHolds the snapshot so it can be queried in SQLWhere snapshots land, so the audit is a SQL query rather than a script walking the hierarchy.

The concept that shapes the whole design: Google's services write as themselves. When Cloud Asset Inventory delivers to your topic or your dataset, the identity doing the writing is Google's own service agent for that product, not you and not your pipeline. Being project owner on the destination is irrelevant. You have to grant the service agent access to your own resources — which feels backwards until you have hit it once, and then you expect it everywhere.

Architecture and Workflow

What exists, and what changed GCP Lab · Week 7 the assets every resource, every policy Icon_24px_Pub-Sub_Color a feed changes, as they happen Icon_24px_BigQuery_Color a snapshot the whole inventory, in SQL notifies exports both are written by Google's Cloud Asset service agent — not by the caller jayanthkatta.com

The flow

  1. Enable Cloud Asset Inventory and create a Pub/Sub topic and BigQuery dataset to receive output.
  2. Grant Google's Cloud Asset service agent publisher rights on the topic and editor rights on the dataset — it writes as itself.
  3. Create an organization-level feed, which publishes a message whenever any asset changes.
  4. Run a snapshot export, which writes the full inventory to BigQuery.
  5. Query the snapshot to audit the hierarchy — what sits outside a folder, what keys exist, what the Terraform never described.
  6. Compare the findings against what the configuration claims.

Two decisions worth naming

The snapshot is a script, not a Terraform resource. An export is an operation with a moment attached; a feed is a desired state. There is no provider gap here — the two things are genuinely different, and forcing the export into Terraform would mean re-running it on every apply.

The feed is organization-scoped. A project-scoped feed would miss exactly the assets this audit exists to find: the ones nobody remembered to put in the right place.

Prerequisites

  • An organization with several projects — ideally some created outside the hierarchy, so the audit has something real to find.
  • roles/cloudasset.owner on the Terraform apply identity. This is a genuine over-grant and the lab says so — see Security.
  • roles/cloudasset.viewer for the operator who runs the snapshot script.
  • cloudasset, pubsub and bigquery APIs enabled on the quota project.
  • Terraform 1.9+ with hashicorp/google ~> 6.0.

Week 1 supplies the hierarchy being audited. Exact grants, variables and queries are in the lab guide.

Implementation

Seven resources and one script. Commands and expected outputs 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 — Create the destinations, then grant the service agent

Why: Cloud Asset Inventory writes as itself. Three of the seven resources exist solely to give Google's service agent access to your own topic, dataset and project.

service-<PROJECT_NUMBER>@gcp-sa-cloudasset.iam.gserviceaccount.com
  roles/pubsub.publisher      on the topic
  roles/bigquery.dataEditor   on the dataset
  roles/bigquery.jobUser      on the project

Validation: the topic's IAM policy names the service agent. Without it the feed is created successfully and then silently delivers nothing.

Google Cloud console showing the Pub/Sub topic that receives asset change notifications.
Validates Step 1: the topic exists and carries the publisher binding for the Cloud Asset service agent.

Step 2 — Create the organization feed

Why: organization-scoped, because a project-scoped feed would miss the assets that are not where they should be — which is the point of the audit.

HCP Terraform run showing Applied with seven resources.
Validates deployment only: seven resources applied. Whether the feed actually delivers is Step 1's grant, and whether the inventory is useful is the next section.

Step 3 — Export a snapshot and audit it

Why: the feed answers "what changed"; the snapshot answers "what is there". The audit needs the second.

./scripts/snapshot.sh
BigQuery showing the asset snapshot table with rows.
Validates Step 3: the snapshot landed with rows, which is what makes the audit a SQL query rather than a guess.

Validation and Evidence

TestExpectedActualStatus
Infrastructure deployed7 resources7 added, 0 changed, 0 destroyedPass
Feed exists and targets the topicfeed resolves, topic matchesboth correctPass
Cloud Asset Inventory can publishservice agent holds pubsub.publisherpresentPass
Snapshot has recent rowsrows > 0, dated todayrows present and currentPass
Audit: projects outside a folderreported, not asserted2 of 7Finding
Audit: user-managed service account keyszerozero — the naive query says 2, both SYSTEM_MANAGEDPass
A change actually produces a Pub/Sub message end to endmessage observed on the topicnot tested — the grant was verified, delivery was notUnverified

What the evidence proves. Rows one to four say the plumbing exists and the snapshot works. Row five is the actual result — the audit found two projects sitting directly under the organization, which no amount of reading the Terraform would have revealed. Row six is a near-miss worth keeping: the obvious query returns two keys and the correct answer is zero, because both are Google-managed. The last row is honest about an untested path: the feed's publisher grant was confirmed, but no change was made and no message observed.

Cloud Asset Inventory dashboard in the Google Cloud console.
Context, with a caveat: the console dashboard is cached — it reported data from six days earlier beside an export minutes old. Query the snapshot, not the page.

Security and Cost

Security decisions

ControlDecision and trade-off
Accepted over-grantThe plan identity holds roles/cloudasset.owner, which can delete a feed, because no read-only role can read one. This is a real hole in the lab's read-only-plan guarantee. Week 8 closes it with a custom role and a deny policy.
Service agent grantsScoped to the specific topic and dataset, not the project, except bigquery.jobUser which has no narrower scope.
Operator accessA human cannot impersonate the apply identity — it is bound to the HCP workload identity pool only — so the snapshot script needed its own read-only grant.
Data exposureThe snapshot contains the full asset inventory, including resource names and IAM policies. The dataset is project-scoped and not shared.

Cost

$0. Asset feeds and exports are free. The snapshot is a few kilobytes of BigQuery storage, well inside the free tier. Measured, not estimated.

Troubleshooting and Lessons

The same service gives an excellent error and a useless one

Problem: the BigQuery export failed with "The caller does not have permission" and nothing else.

Root cause: the Cloud Asset service agent lacked bigquery.dataEditor on the dataset. The identical class of mistake on the Pub/Sub side produces an error naming the service account, the permission and the resource.

Resolution: grant the service agent, not the caller.

Lesson: error quality varies within a single service. Do not assume a vague message means a subtle problem; it may be the same missing grant, reported badly.

No read-only role can read an asset feed

Problem: the plan phase could not refresh the feed resource.

Root cause: roles/cloudasset.viewer lacks cloudasset.feeds.get. Only cloudasset.owner has it, and that role also grants delete.

Resolution: the plan identity was given owner, and the hole was recorded rather than hidden. Week 8 replaces it with a two-permission custom role plus a deny policy.

Lesson: when no predefined role fits, say so in writing at the time. The alternative is an over-grant nobody remembers making.

The dashboard lied about freshness

Problem: the Asset Inventory console showed data six days old next to an export that had just completed.

Root cause: the dashboard is cached.

Lesson: query the snapshot table. A console page is a rendering, not the data.

Cleanup and Key Takeaways

Cleanup

terraform destroy removes all seven resources. Verify against the cloud: the feed should disappear from gcloud asset feeds list --organization=<ORG_ID>, and the topic and dataset should be gone.

Consider keeping it. Feeds and exports are free, and Week 8 is specifically about narrowing access to this feed — destroying Week 7 removes what Week 8 operates on. Commands are in the lab guide.

Ongoing charges: none.

Key takeaways

  1. Google's services write to your resources as themselves. Grant the service agent, not the caller. Expect this for anything that delivers somewhere on your behalf.
  2. An inventory answers questions a configuration cannot. Two projects sat outside the hierarchy; the Terraform had nothing to say about them because it never created them.
  3. Filter on keyType. The obvious service-account-key query returns Google-managed keys too, so "two keys" became "zero user-managed keys" once filtered.
  4. A feed is a state; an export is an operation. One belongs in Terraform and one belongs in a script, and conflating them means re-exporting on every apply.

References

Comments

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