{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "xbill",
  "title_listed": "What Nobody Is Using in Your Google Cloud Projects, and What It Costs",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "工程实践",
  "is_paid_flag": "否",
  "url": "https://dev.to/gde/what-nobody-is-using-in-your-google-cloud-projects-and-what-it-costs-1k0",
  "publish_date": "2026-09-22",
  "corpus_stage": "",
  "notes": "",
  "id": "087bcbd02b3b",
  "collected_at": "2026-10-02T13:51:19",
  "title": "What Nobody Is Using in Your Google Cloud Projects, and What It Costs - DEV Community",
  "body_markdown": "This article provides a step by step guide to building a Google Cloud waste scanner from source, running it across every project your credentials can see, pricing each finding from the Cloud Billing Catalog API, and drafting the cleanup. A suite of Python checks is built to cover Compute Engine, Cloud SQL, Storage, DNS, KMS, Secret Manager, Artifact Registry, Filestore, Cloud Logging, Cloud Monitoring and GKE.\nhttps://github.com/xbill9/zombiescan-gcp\nWhat Gets Left Behind\nA Persistent Disk survives the instance it was attached to. A static IP outlives the migration that freed it. A GKE cluster keeps charging its management fee after the last node pool scaled to zero. Each one bills every hour and reports nothing.\nThe billing console shows the total. The Recommender surfaces candidates. A figure that drives a decision names one resource, in one project, and its monthly cost.\nzombiescan\nproduces that figure for 20 classes of resource across two packs.\nAt This Point You Should Have…\nA Google Cloud account and Application Default Credentials, from\ngcloud auth application-default login\nuv\non the path, and Python 3.11 or newer\nRead access to the projects you mean to scan — the predefined\nroles/viewer\ncovers every call, and\nzombiescan-scanner-role.yaml\n, under\npolicy/\nin the repository, is a 16-permission custom role that covers exactly as much\nStep 1 — Build It From Source\ngit clone https://github.com/xbill9/zombiescan-gcp\ncd\nzombiescan-gcp\nuv\nsync\nEnter fullscreen mode\nExit fullscreen mode\nResolved 39 packages in 0.58ms\nChecked 38 packages in 0.19ms\nEnter fullscreen mode\nExit fullscreen mode\nuv sync\nreads the committed\nuv.lock\n, so the resolved set is the one the tests ran against.\nuv run zombiescan\n--version\nEnter fullscreen mode\nExit fullscreen mode\nzombiescan, version 0.1.0\nEnter fullscreen mode\nExit fullscreen mode\nStep 2 — Install It as a Tool\nWorking from the clone keeps\nuv run\nin front of every command. Installing it puts\nzombiescan\non the path instead.\nuv tool\ninstall\ngit+https://github.com/xbill9/zombiescan-gcp\nEnter fullscreen mode\nExit fullscreen mode\nBoth routes run the same engine. The rest of this article uses the installed form.\nStep 3 — Authenticate\ngcloud auth application-default login\nEnter fullscreen mode\nExit fullscreen mode\ngoogle.auth.default()\npicks that up. No key file is downloaded, and no service account is created.\nEvery worker thread in a scan shares one credentials object, and the first token is fetched before the threads start. Refreshing credentials signs through OpenSSL, and one credentials object refreshed from several threads at once ends the process instead of raising, so the wrapper serialises the refreshes behind a lock.\nStep 4 — List the Checks\nzombiescan checks\nEnter fullscreen mode\nExit fullscreen mode\ndisabled-kms-key  Disabled KMS key versions still billing  (core)\n  empty-vpc-network  VPC networks with nothing running in them  (core)\n  gke-idle-cluster  GKE clusters running no nodes  (gke)\n  idle-cloud-nat  Cloud NAT gateways serving no instances  (core)\n  idle-filestore  Filestore instances nothing can mount  (core)\n  idle-forwarding-rule  Load balancers with no backends  (core)\n  orphaned-snapshot  Snapshots of deleted disks  (core)\n  stale-artifact-repository  Artifact Registry repositories with no recent pushes  (core)\n  stale-secret  Secrets nothing has updated in 90 days  (core)\n  stopped-instance  Stopped instances still paying for disks  (core)\n  stopped-sql-instance  Stopped Cloud SQL instances still paying for storage  (core)\n  unattached-disk  Unattached Persistent Disks  (core)\n  unbounded-log-bucket  Log buckets retaining logs indefinitely  (core)\n  unmanaged-gcs-bucket  Versioned buckets with no lifecycle rule  (core)\n  unused-dns-zone  Cloud DNS zones publishing nothing  (core)\n  unused-firewall-rule  Firewall rules matching nothing  (core)\n  unused-image  Custom images nothing boots from  (core)\n  unused-static-ip  Unused static IP addresses  (core)\n  unused-subnet  Subnets reserving ranges nothing uses  (core)\n  unused-uptime-check  Uptime checks monitoring deleted resources  (core)\nEnter fullscreen mode\nExit fullscreen mode\nNineteen ship in the\ncore\npack and one in\ngke\n. GKE sits in its own pack because it carries its own API, its own rate section and its own price fetcher, which is what a third-party pack has to supply.\nStep 5 — Which APIs the Checks Need\nzombiescan apis\nEnter fullscreen mode\nExit fullscreen mode\nartifactregistry.googleapis.com  1 check(s)\n  cloudkms.googleapis.com  1 check(s)\n  compute.googleapis.com  12 check(s)\n  container.googleapis.com  1 check(s)\n  dns.googleapis.com  1 check(s)\n  file.googleapis.com  1 check(s)\n  logging.googleapis.com  1 check(s)\n  monitoring.googleapis.com  1 check(s)\n  secretmanager.googleapis.com  1 check(s)\n  sqladmin.googleapis.com  1 check(s)\n  storage.googleapis.com  1 check(s)\n\nAn API that is not enabled on a project is skipped, not reported as an error: a\nproject that never used a service has no waste in it.\nEnter fullscreen mode\nExit fullscreen mode\nEach check declares the APIs it calls. That declaration generates this list and the read-only custom role, and the test suite fails when a check names an API the role does not cover.\nStep 6 — Scan One Project\nzombiescan scan\n--project\nglitnir-dev\nEnter fullscreen mode\nExit fullscreen mode\nScanning as application default credentials\n1 project(s), 20 check(s) — read-only\n\nzombiescan — 2 findings across 1 project\n\nCheck                      Found  Monthly\nstale-artifact-repository      1    $0.01\nempty-vpc-network              1    $0.00\nEnter fullscreen mode\nExit fullscreen mode\nEvery call is a list or a get. The scan has no code path that deletes, modifies or releases anything.\nStep 7 — Scan Every Project\nzombiescan scan\n--all-projects\nEnter fullscreen mode\nExit fullscreen mode\nScanning as application default credentials\n75 project(s), 20 check(s) — read-only\n\nzombiescan — 713 findings across 75 projects\n\nCheck                      Found  Monthly\nunattached-disk               36  $153.53\nunused-static-ip              12   $89.06\nunused-image                  64   $75.25\nstopped-instance              12   $35.76\norphaned-snapshot             39   $28.66\nstale-artifact-repository     13    $4.75\nstale-secret                   9    $0.54\nunused-dns-zone                1    $0.20\nempty-vpc-network             13    $0.00\nunmanaged-gcs-bucket         118    $0.00\nunused-firewall-rule          43    $0.00\nunused-subnet                349    $0.00\nunused-uptime-check            4    $0.00\nEnter fullscreen mode\nExit fullscreen mode\nEstimated waste: $387.74/month ($4,652.87/year)\nEnter fullscreen mode\nExit fullscreen mode\n--all-projects\nasks Resource Manager's\nprojects.search\nwhich projects the caller can see, then fans out over them. 75 projects times 20 checks is 1,500 project/check pairs, and 411 of them were skipped because the API was not enabled on that project. The sweep took 79 seconds.\nThirteen of the 20 checks fired. Five of those thirteen classes carry no charge and account for 527 of the 713 findings: empty VPC networks, versioned buckets with no lifecycle rule, firewall rules matching nothing, unused subnets and uptime checks monitoring deleted resources. They are reported because they accumulate without limit and block deletions.\nStep 8 — Read the Per-Resource Table\nThe detail table sorts the costliest findings first and guarantees a row to every check that fired.\n┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓\n┃ Project        ┃ Location                ┃ Resource               ┃  Monthly ┃\n┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩\n│ comglitn       │ global                  │ w5-opt                 │   $53.19 │\n│ glitnir-ba1    │ europe-west1-b          │ disk-20260326-160533   │   $50.00 │\n│ glitnir-ba1    │ europe-west1-c          │ disk-20260326-152217   │   $50.00 │\n│ glitnir-mb1    │ us-east1-c              │ mb1                    │   $21.76 │\n│ comglitn       │ us-central1-c           │ disk-opt-w1            │   $20.00 │\n│ xbill-b1       │ us                      │ snapshot-b1-east       │   $16.57 │\n│ comglitn       │ us-central1-c           │ w2                     │   $10.88 │\n│ glitnir-sx1    │ us-east4                │ sx2                    │    $8.03 │\n│ swoonbox1      │ us-east4                │ swoonbox-landing-4     │    $8.03 │\n│ comglitn       │ us-central1             │ w1                     │    $7.30 │\n└────────────────┴─────────────────────────┴────────────────────────┴──────────┘\nEnter fullscreen mode\nExit fullscreen mode\nProject is the first column because a resource id is unique inside a project and nowhere else. Location holds a zone, a region or\nglobal\n, read off each resource.\nA\n~\nmarks an estimate or an upper bound, and the per-finding\nnote\nin JSON output names the reason.\n🔎 Tip:\n--min-cost 5\nhides findings under five dollars a month,\n--limit 0\nprints every row instead of the top 25, and\n--location us-central1\nkeeps a region's zones along with the region itself.\nStep 9 — Where a Check Runs\nA check runs once per project. Two properties of Google's APIs make that a complete sweep.\nCompute Engine's\naggregatedList\nreturns disks, addresses, instances, routers, subnetworks and forwarding rules across every zone and region in one call. Most other APIs accept\nlocations/-\n, a wildcard meaning every location.\nCloud KMS and Artifact Registry reject\nlocations/-\n. Those two enumerate the API's locations and walk them in parallel, and each worker gets its own client, because the HTTP connection underneath a discovery client cannot be shared between threads.\nStep 10 — Where the Prices Come From\nThe bundled price table is generated from the Cloud Billing Catalog API.\npython\n-m\nzombiescan.pricing.refresh\nEnter fullscreen mode\nExit fullscreen mode\nA scan reads\ntable.json\nfrom the installed package, so it runs offline and adds no latency. Rates are looked up by key and region:\nctx.pricing.rate(\"disk.gb_month\", region=...)\n.\nVerified rates in us-central1, from the bundled table:\nRate\nus-central1\npd-standard\n$0.04/GB/month\npd-balanced\n$0.10/GB/month\npd-ssd\n$0.17/GB/month\nSnapshots and custom images\n$0.05/GB/month\nIdle static IP\n$0.010/hour\nForwarding rule minimum\n$0.025/hour\nGKE cluster\n$0.10/hour\nSecret version\n$0.06/month\nKMS software key version\n$0.06/month\nKMS HSM key version\n$1.00/month\nArtifact Registry storage\n$0.10/GB/month\nAt 730 hours to the month, arithmetic over those hourly rates gives $7.30 a month for an idle static IP, $18.25 for a forwarding rule, and $73.00 for a GKE cluster with no nodes on it.\nStep 11 — Read the First Tier That Charges\nGoogle fronts many SKUs with a free allowance priced at zero: the first 30 GB of standard Persistent Disk, the first 0.5 GB of Artifact Registry, the first six secret versions. A fetcher that reads tier 0 records the rate as free, which prices every finding in that section at nothing and reports a clean project.\nunit_price()\ntakes the first tier that charges. Reading a named tier deliberately — the way the Cloud DNS fetcher walks the zone tiers at $0.20 for the first 25 and $0.10 beyond them — is a separate call.\nA SKU published against the region\nglobal\nhas no per-region entry at all. Cloud NAT addresses, Artifact Registry storage and log retention are all published that way, so they live in a global section with a\nscope=\"global\"\nrate spec.\nThe refresher refuses to write a table that loses or empties a section the previous one had, because an empty section means a matcher stopped matching.\nStep 12 — JSON Output\nzombiescan scan\n--project\nglitnir-dev\n--json\nfindings.json\nEnter fullscreen mode\nExit fullscreen mode\n{\n\"schema_version\"\n:\n3\n,\n\"scan\"\n:\n{\n\"generated\"\n:\n\"2026-09-21T19:55:55Z\"\n,\n\"duration_seconds\"\n:\n5.41\n,\n\"principal\"\n:\n\"application default credentials\"\n,\n\"projects\"\n:\n[\n\"glitnir-dev\"\n],\n\"pairs_attempted\"\n:\n20\n,\n\"pairs_unavailable\"\n:\n3\n,\n\"complete\"\n:\ntrue\n},\n\"pricing\"\n:\n{\n\"generated\"\n:\n\"2026-09-21T16:35:39Z\"\n,\n\"basis\"\n:\n\"Google Cloud Billing Catalog API, on-demand USD list prices\"\n,\n\"excludes\"\n:\n[\n\"committed use discounts\"\n,\n\"sustained use discounts\"\n,\n\"private pricing\"\n,\n\"credits\"\n]\n},\n\"totals\"\n:\n{\n\"monthly_cost\"\n:\n0.01\n,\n\"annual_cost\"\n:\n0.12\n,\n\"finding_count\"\n:\n2\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\nEach finding carries its own reason, cost, remediation command and a\ndetails\nblock:\n{\n\"check\"\n:\n\"stale-artifact-repository\"\n,\n\"resource_id\"\n:\n\"gcr.io\"\n,\n\"project\"\n:\n\"glitnir-dev\"\n,\n\"location\"\n:\n\"us\"\n,\n\"reason\"\n:\n\"DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days\"\n,\n\"monthly_cost\"\n:\n0.01\n,\n\"approximate_cost\"\n:\ntrue\n,\n\"remediation\"\n:\n\"gcloud artifacts repositories delete gcr.io --location=us --project=glitnir-dev --quiet\"\n,\n\"details\"\n:\n{\n\"stored_gb\"\n:\n0.1\n,\n\"idle_days\"\n:\n282\n,\n\"usd_per_gb_month\"\n:\n0.1\n,\n\"note\"\n:\n\"upper bound: Artifact Registry bills each unique layer once, and images sharing a base layer are counted once per image here\"\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\nThe pricing block dates the table and lists what list prices exclude, so a report read six months later says which rates produced it.\nStep 13 — The HTML Report\nzombiescan scan\n--project\nglitnir-dev\n--html\nreport.html\nEnter fullscreen mode\nExit fullscreen mode\nOne self-contained file with the styles inline, which prints to PDF without fetching anything.\nStep 14 — The Cleanup Plan\nzombiescan scan\n--project\nglitnir-dev\n--script\ncleanup.sh\nEnter fullscreen mode\nExit fullscreen mode\n#!/usr/bin/env bash\n# zombiescan cleanup plan — generated 2026-09-21T19:55:55Z\n#\n# READ EVERY LINE BEFORE RUNNING THIS.\n# zombiescan generated this file and did not run it. Deleting Google\n# Cloud resources is not reversible. Where a backup is possible the\n# command takes one first, but a backup is not a substitute for\n# knowing what you are deleting.\n#\n# 2 resource(s), about $0.01/month.\nset\n-euo\npipefail\n# glitnir-dev us gcr.io — DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days\n# saves about $0.01/month\ngcloud artifacts repositories delete gcr.io\n--location\n=\nus\n--project\n=\nglitnir-dev\n--quiet\n# glitnir-dev global default — VPC network runs no instances; it still holds none. This is the auto-created default network\n# saves about $0.00/month\ngcloud compute networks delete default\n--project\n=\nglitnir-dev\n--quiet\nEnter fullscreen mode\nExit fullscreen mode\nEvery generated command carries\n--project\nand\n--quiet\n, and interpolated resource ids are shell-quoted. The test suite checks all three at the source, because a remediation command is printed for an operator to run.\nStep 15 — Cleaning Up\nclean\nis a separate command, and a dry run is what it does with no flags.\nzombiescan clean\n--project\nglitnir-dev\nEnter fullscreen mode\nExit fullscreen mode\nScanning as application default credentials — 1 project(s)\n\ndry run — 1 of 2 finding(s) can be cleaned, 1 cannot\n  skip empty-vpc-network default: a network cannot be deleted until everything\ninside it is gone -- subnets, routes, firewall rules, Cloud Routers, peerings\nand any Private Service Connect attachment -- and the order depends on what else\nreferences them. Clean the findings inside the network first; this one goes away\nwith them\n\nstale-artifact-repository  gcr.io  glitnir-dev · us · $0.01/mo\n  DOCKER repository holding 0.1 GB with nothing pushed to it in 282 days\n    → IRREVERSIBLE delete Artifact Registry repository gcr.io and its images\n      artifactregistry.projects.locations.repositories.delete({'name':\n'projects/glitnir-dev/locations/us/repositories/gcr.io'})\n\nWould free about $0.01/month across 1 resource(s); 1 include irreversible steps.\nNothing was changed. Re-run with --apply to perform these.\nEnter fullscreen mode\nExit fullscreen mode\n--apply\ngates one thing: whether a planned step is sent to Google Cloud. It never changes which steps get planned, so the preview above is what runs.\nPlanning is read-only. A cleaner may read — fetching an instance's disks to clear\nautoDelete\n, reading a router's NAT list — and it yields the mutations as objects for the runner to send.\nWhere the API allows a backup, the backup step is ordered before the destruction, and a failed step stops the rest of that finding. A failed snapshot can never be followed by the delete that assumed it.\nIRREVERSIBLE\nmarks a step with no recovery window. A released static IP is gone, so it carries the mark; a destroyed KMS key version is held for 24 hours, so it does not.\nA check with no cleaner reports the finding as unsupported with a reason, which is the\nskip\nline above. The test suite fails a check that has neither a cleaner nor a reason.\n🔎 Tip:\nclean --from findings.json\nacts on a report already reviewed, and refuses a report produced by different credentials.\nStep 16 — The Read-Only Role\ngcloud iam roles create zombiescanScanner\n--project\n=\nPROJECT_ID\n\\\n--file\n=\npolicy/zombiescan-scanner-role.yaml\n--quiet\nEnter fullscreen mode\nExit fullscreen mode\nSixteen permissions, every one a list or a get. The file is generated from the\napis=\neach check declares, and the suite keeps it in step.\nclean\nneeds more than this, deliberately: the identity that reports waste should be unable to delete what it reports.\nStep 17 — The Claude Code Plugin\n/plugin marketplace add xbill9/zombiescan-gcp\nEnter fullscreen mode\nExit fullscreen mode\nThe plugin ships two slash commands, a skill and an MCP server over the same engine.\nscan_project  Scan Google Cloud projects for unused resources and price them\n  estimate_savings  Total, count and break down the findings in a report\n  explain_finding  Explain why a check treats a resource as waste\n  list_checks  List the installed checks\n  plan_cleanup  Show exactly what `zombiescan clean` would do\nEnter fullscreen mode\nExit fullscreen mode\nEvery tool is read-only.\nplan_cleanup\nbuilds the step objects and stops there; the function that sends them to Google Cloud is unreachable from the server, and a test asserts the module names no other\nclean.*\nattribute.\nThe tools return computed figures — totals, counts, breakdowns, cheapest and costliest — and echo the filter they applied. A tool that returned rows for the model to add up would move the arithmetic to the place least able to do it.\nThe protocol is JSON-RPC over stdio in the standard library, with no MCP SDK dependency for about a hundred lines of framing.\nThree Findings That Differ From the AWS Instinct\nResource\nGoogle Cloud behaviour\nIdle Cloud NAT\nGateway uptime bills per VM using it, so a gateway serving nothing costs the addresses it holds — $3.65 a month each at $0.005/hour. An AWS NAT gateway bills a flat hourly charge whatever uses it.\nGKE cluster\n$0.10/hour whatever runs on it. Scaling every node pool to zero removes the node cost and leaves the $73.00 a month management fee.\nReserved static IP\nGoogle bills an idle static IP at a higher hourly rate than one attached to a running instance, so the waste costs more than the work.\nWhat the Checks Leave Alone\nA false positive here costs an outage, so each check states the condition it declines to act on.\nCondition\nTreatment\nLoad balancer with backends that are unhealthy\nAn outage; left alone\nSnapshot whose source disk still exists\nLoad-bearing; left alone\nDisk attached to a stopped instance\nReported under\nstopped-instance\n, counted once\nGoogle-managed KMS key, disabled\nFree; never reported\nKey or secret already scheduled for destruction\nAlready on a timer; left alone\nDefault VPC network with nothing in it\nReported, and flagged as the auto-created default\nAn API not enabled on a project\nSkipped; a service never used holds no waste\nempty-vpc-network\nreports without a cleanup step. A network deletes only after its subnets, routes, firewall rules, Cloud Routers and peerings are gone, in an order that depends on what references them, so the finding says so where a command would otherwise appear.\nFour of the 75 projects returned\npermission denied: no access\non every check, producing 42 skipped project/check pairs. Those appear in the report as a list, and the monthly total covers the projects that were readable.\nCompare and Contrast\nzombiescan\nRecommender / Active Assist\nBilling reports\nCross-project SaaS\nPer-resource dollar figure\n🥇 yes\npartial\naggregate only\nyes\nCredentials leave the machine\n🥇 never\nn/a, Google-side\nn/a, Google-side\n❌ service account granted\nDeletes on request\n🥈 opt-in, dry run first\n❌ no\n❌ no\n🥇 yes\nPrice source\nBilling Catalog API, dated\nGoogle-side\nyour bill\nvendor\nRuns offline after install\n🥇 yes\n❌ no\n❌ no\n❌ no\nBreadth\n20 checks\nbroader\nn/a\nbroader\nSo, Which One?\nBilling reports answer what the projects spent. Recommender covers more ground and reaches areas outside these 20 checks, including the metric-driven judgements that need a lookback window.\nzombiescan\nfits the case where the answer has to be per-resource, priced, and produced without granting anything access to the projects. A laptop, existing credentials, and about 80 seconds for 75 projects.\nCost\nA scan costs nothing. List and get calls carry no charge, and the price table ships with the package, so a scan makes no Billing Catalog call.\nRegenerating the table calls the Cloud Billing Catalog API, which is also free.\nTests\nuv run pytest\n-q\nEnter fullscreen mode\nExit fullscreen mode\n324 passed, 4 deselected in 0.37s\nEnter fullscreen mode\nExit fullscreen mode\nThe suite runs offline against recorded API responses, with no credentials. The four deselected tests reach a real project and run under\nZOMBIESCAN_LIVE=1\n.\nRecorded responses are keyed the way the discovery client returns them, and every aggregated one includes a scope holding only a\nwarning\n, which is how Compute Engine reports an empty zone. A check that walks past that shape without skipping it fails here instead of in production.\nTeardown\nuv tool uninstall zombiescan-gcp\nEnter fullscreen mode\nExit fullscreen mode\nNothing is left in any project. The tool creates no service account, no role, no bucket and no stored state. Revoke the local credentials with\ngcloud auth application-default revoke\n.\nSummary\nThe goal of this article was to audit every Google Cloud project a set of credentials can reach and attach a monthly cost to each unused resource. The key to the solution was fanning out per project instead of per region, and fetching every rate from the Cloud Billing Catalog API while keeping the scan read-only. The results were:\n🟢 20 checks in two packs across Compute Engine, Cloud SQL, Storage, DNS, KMS, Secret Manager, Artifact Registry, Filestore, Cloud Logging, Cloud Monitoring and GKE\n🟢 75 projects swept in 79 seconds, 713 findings, $387.74 a month and $4,652.87 a year\n🟢 One call per project:\naggregatedList\ncovers every zone and region, and\nlocations/-\ncovers every location for the APIs that accept it\n🟢 Prices dated in every report, with the first charging tier read instead of a free tier 0\n🟢 JSON against a published schema, a self-contained HTML report, and a shell cleanup plan that is printed and never run\n🟢\nclean\npreviews by default, orders a backup before the destruction it protects, and marks steps with no recovery window\n⚠️ 411 of 1,500 project/check pairs were skipped because the API was not enabled, and 42 more for lack of access to 4 projects, so $387.74 covers the projects that were readable\n⚠️ Figures are on-demand list prices, excluding committed use discounts, sustained use discounts, private pricing and credits\n⚠️ Artifact Registry is an upper bound: it bills a shared layer once, and a finding counts it once per image\n❌\nempty-vpc-network\nreports without a cleanup step, because a network needs its dependencies removed in an order that depends on what references them\n❌ Idle judgements that need a lookback window — Cloud SQL by connection count, KMS keys by audit log — need Cloud Monitoring and are outside these 20 checks\nScope: one set of Application Default Credentials, 75 projects reachable through\nprojects.search\n, 20 checks, a single sweep per figure quoted, run from one laptop. The read-only guarantee holds for the packs in this repository, which are reviewed; a pack installed from PyPI runs with the same credentials and carries no such guarantee.\nThe strategy for using per-resource pricing for Google Cloud waste detection was validated with an incremental step by step approach.\nReferences\nRepository, MIT:\nhttps://github.com/xbill9/zombiescan-gcp\nFindings JSON Schema:\nhttps://github.com/xbill9/zombiescan-gcp/blob/main/docs/findings.schema.json\nPack author guide:\nhttps://github.com/xbill9/zombiescan-gcp/blob/main/docs/PACKS.md\nCloud Billing Catalog API,\nservices.skus.list\n:\nhttps://cloud.google.com/billing/docs/reference/rest/v1/services.skus/list\nCompute Engine\naggregatedList\n:\nhttps://cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList\nVPC pricing, external IP addresses:\nhttps://cloud.google.com/vpc/network-pricing\nGKE cluster management fee:\nhttps://cloud.google.com/kubernetes-engine/pricing\nApplication Default Credentials:\nhttps://cloud.google.com/docs/authentication/application-default-credentials",
  "metadata": {
    "title": "What Nobody Is Using in Your Google Cloud Projects, and What It Costs - DEV Community",
    "description": "A local-first CLI that scans Google Cloud projects for unused resources, prices each one from the Cloud Billing Catalog API, and drafts the cleanup. One call per project instead of one per region, read-only by default, and a Claude Code plugin over the same engine. Tagged with googlecloud, python, devops, opensource.",
    "keywords": "googlecloud, python, devops, opensource, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/gde/what-nobody-is-using-in-your-google-cloud-projects-and-what-it-costs-1k0",
    "og:title": "What Nobody Is Using in Your Google Cloud Projects, and What It Costs",
    "og:description": "A local-first CLI that scans Google Cloud projects for unused resources, prices each one from the Cloud Billing Catalog API, and drafts the cleanup. One call per project instead of one per region, read-only by default, and a Claude Code plugin over the same engine.",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fzombiescan-gcp%2Fmain%2Farticles%2Fzombiescan-gcp%2Fdevto-cover.4e999e75.jpg",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@",
    "twitter:title": "What Nobody Is Using in Your Google Cloud Projects, and What It Costs",
    "twitter:description": "A local-first CLI that scans Google Cloud projects for unused resources, prices each one from the Cloud Billing Catalog API, and drafts the cleanup. One call per project instead of one per region, read-only by default, and a Claude Code plugin over the same engine.",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fzombiescan-gcp%2Fmain%2Farticles%2Fzombiescan-gcp%2Fdevto-cover.4e999e75.jpg"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/087bcbd02b3b/code/body-000.txt",
      "chars": 77,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-001.txt",
      "chars": 61,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-002.txt",
      "chars": 28,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-003.txt",
      "chars": 26,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-004.txt",
      "chars": 61,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-005.txt",
      "chars": 38,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-006.txt",
      "chars": 18,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-007.txt",
      "chars": 1328,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-008.txt",
      "chars": 16,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-009.txt",
      "chars": 558,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-010.txt",
      "chars": 38,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-011.txt",
      "chars": 251,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-012.txt",
      "chars": 31,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-013.txt",
      "chars": 718,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-014.txt",
      "chars": 48,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-015.txt",
      "chars": 1134,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-016.txt",
      "chars": 37,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-017.txt",
      "chars": 59,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-018.txt",
      "chars": 606,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-019.txt",
      "chars": 596,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-020.txt",
      "chars": 57,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-021.txt",
      "chars": 58,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-022.txt",
      "chars": 843,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-023.txt",
      "chars": 39,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-024.txt",
      "chars": 926,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-025.txt",
      "chars": 118,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-026.txt",
      "chars": 46,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-027.txt",
      "chars": 321,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-028.txt",
      "chars": 17,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-029.txt",
      "chars": 34,
      "language": "text"
    },
    {
      "file": "attachments/087bcbd02b3b/code/body-030.txt",
      "chars": 33,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://github.com/xbill9/zombiescan-gcp",
      "label": "https://github.com/xbill9/zombiescan-gcp",
      "type": "a"
    },
    {
      "href": "https://github.com/xbill9/zombiescan-gcp/blob/main/docs/findings.schema.json",
      "label": "https://github.com/xbill9/zombiescan-gcp/blob/main/docs/findings.schema.json",
      "type": "a"
    },
    {
      "href": "https://github.com/xbill9/zombiescan-gcp/blob/main/docs/PACKS.md",
      "label": "https://github.com/xbill9/zombiescan-gcp/blob/main/docs/PACKS.md",
      "type": "a"
    },
    {
      "href": "https://cloud.google.com/billing/docs/reference/rest/v1/services.skus/list",
      "label": "https://cloud.google.com/billing/docs/reference/rest/v1/services.skus/list",
      "type": "a"
    },
    {
      "href": "https://cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList",
      "label": "https://cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList",
      "type": "a"
    },
    {
      "href": "https://cloud.google.com/vpc/network-pricing",
      "label": "https://cloud.google.com/vpc/network-pricing",
      "type": "a"
    },
    {
      "href": "https://cloud.google.com/kubernetes-engine/pricing",
      "label": "https://cloud.google.com/kubernetes-engine/pricing",
      "type": "a"
    },
    {
      "href": "https://cloud.google.com/docs/authentication/application-default-credentials",
      "label": "https://cloud.google.com/docs/authentication/application-default-credentials",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:27Z",
  "source_url": "https://dev.to/gde/what-nobody-is-using-in-your-google-cloud-projects-and-what-it-costs-1k0",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 25095,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/087bcbd02b3b/pages/source.html",
  "local_html": "attachments/087bcbd02b3b/pages/article.html",
  "body_base_path": "attachments/087bcbd02b3b/pages/",
  "body_markdown_path": "attachments/087bcbd02b3b/pages/article.md"
}