Case Study

GCP Spotlight

A FinOps platform I built to find waste across an entire GCP organization. Four service modules (Kubernetes, virtual machines, storage and AI) share one cost model, one hub dashboard and one question: what should we fix first?

~600 GCP Projects
59 GKE Clusters
2,222 Compute VMs
2,989 Storage Buckets

Waste With Four Owners

The data existed in GCP’s APIs. Nobody had connected it.

~600Projects, each a place for over-provisioned pods, idle VMs and unread buckets to hide
4Service types, each with its own console, its own mental model and its own responsible team
0Places to answer where the waste is across the organization, and what to fix first

The Cost Model Comes First

Every recommendation is a dollar claim, so every module reads from the same three-tier pricing engine.

1. Billing Export

Real invoiced cost from the BigQuery billing export, including Committed Use and Sustained Use credits. Cached for 6 hours. Labeled “Billing.”

2. Derived Rates

Where billing data exists, per-core and per-GB rates are actual cost divided by capacity, reflecting real machine types, regions and discounts. Labeled “Estimated.”

3. On-Demand Fallback

A machine-type catalog of 9 families (e2 through t2d) with regional multipliers, and generic on-demand pricing when the type is unknown. Labeled “Estimated.”

One Module At A Time

Each resource type has its own signals and its own rules, so each gets its own module.

GKE: Kubernetes

59 Clusters, every pod measured
Stability ClassVariability (CV)Headroom Applied
StableUnder 25%25% above P95
Variable25 to 50%35% above P95
SpikyOver 50%45% above P95
  • A flat “P95 plus 20%” fails when some workloads spike 5× and others are perfectly flat, so each workload is classified by its coefficient of variation.
  • A 70% waste recovery rate recommends capturing most of the savings, not all of it, to leave a practical safety margin.
  • Recommendations end in a patch. Bulk YAML generation with multi-select and preview bridges “identify waste” and “fix it,” and a fleet efficiency score carries a 7-day trend forecast.
  • Label compliance is tracked because untagged workloads cannot be allocated to anyone.

GCE: Compute Engine

2,222 Virtual machines
SignalRuleRecommendation
IdleBelow 5% CPU utilizationFlag the instance
OversizedUsage fits a smaller machine typeDownsize
InterruptibleSpot candidate scoreConvert to Spot
Business hours onlyUsage confined to working hoursAuto-schedule
Steady loadCUD coverage gap1-year and 3-year savings projections

GCS: Cloud Storage

2,989 Buckets
SignalRuleOutput
AbandonedNo reads in 90 or more daysCandidate for cleanup
Wrong classAccess pattern does not match storage classDownloadable lifecycle policy JSON
ExposurePublic access enabledRisk flag
VersioningOld versions accumulatingWaste tracked
EgressOutbound transfer costTrend monitoring

AI: Vertex AI

2× The rolling average that flags an anomalous day
SignalRuleOutput
Model spendBigQuery billing export, by modelSpend by model
Cheaper optionCost per token across modelsAlternative recommendation
AnomalyA day above 2× the rolling averageFlagged day
AttributionSpend grouped by projectChargeback view

Hub, Project Pages And ARIA

Four modules answer four questions. Three layers on top answer the one that matters.

Collect In Parallel, Store Apart

Three collectors run side by side into three separate databases.

GCP APIs

Cloud Monitoring, Container, Compute Engine, Cloud Storage and the BigQuery billing export.

Three Collectors

GKE, GCE and GCS run twice daily on a configurable schedule, in parallel with asyncio.gather, behind a concurrency lock.

Three SQLite Files

One database per service in WAL mode, with 1-year retention that prunes data older than 365 days.

API And UI

FastAPI with 123 endpoints feeding a React app of 13 pages and 61 components.

Correctness Under Failure

A dedicated pass after daily use found bugs that only appear under conditions you do not hit while developing.

ConditionWhat Went WrongWhat I Changed
A pod dies mid-runThe single-flight guard was a plain thread lock that wedges forever if its holder dies; every later collection returned 409A flag with a timestamp that auto-reclaims after two hours, longer than any plausible run
A restart interrupts a runThreads killed on shutdown left run records stuck “in progress,” so status endpoints reported a collection that would never finishOn startup, any run still in progress is marked failed with an “interrupted by restart” reason
A resource is decommissionedLatest-snapshot views kept showing deleted pods and VMs as though they still existedA 48-hour staleness cutoff on every “current” view
Same names in two placesGrouping by resource name alone collided same-named resources across projects and zonesFull natural keys: project, zone and instance, or project, cluster, namespace and pod
Two requests raceA billing cache refresh cleared and refilled a dictionary, so a concurrent reader saw half a cacheBuild the full result first, then swap the reference under a lock
Load arrivesSynchronous database work inside async handlers blocked the event loopRead routes are plain synchronous functions run in the threadpool; only truly awaiting routes stayed async

None of this is visible in a demo.

All of it is the difference between a tool people use once and a tool people rely on.

Read-Only Across 600 Projects

Terraform applies identical IAM bindings to every monitored project in a single plan and apply.

ControlSetting
PermissionsViewer roles only (container, monitoring, compute) plus a custom storage role; the app cannot modify any cloud resource
Storage RoleCustom, limited to buckets.list/get/getIamPolicy and objects.list, replacing storage.objectViewer
CredentialsWorkload Identity maps the Kubernetes service account to a GCP IAM principal; no keys are stored or written to disk
RuntimeNon-root container (UID 1001) on a minimal base image
NetworkNo public internet exposure; VPN-only on the internal corporate network
TraceabilityStructured JSON logs with a correlation ID on every request
AuditabilityEvery permission grant is version-controlled in the Terraform module and reviewed in pull requests
OnboardingA new GCP project is a one-line Terraform variable addition

Harder Than The Code

The hardest problems were data problems, not technical ones.

What I Would Change

React 18 TypeScript Vite TanStack Query Recharts Tailwind CSS Python 3.11 FastAPI SQLAlchemy Alembic SQLite BigQuery Terraform Docker Kubernetes ArgoCD

The most valuable FinOps tool is the one that makes the invisible visible.

Most of the waste this platform identifies was technically knowable before. It just was not surfaced in a way that made action easy.
 Back to Projects