Case Study

GCP Spotlight

A unified FinOps platform I built to analyze cloud resource efficiency across an entire GCP organization — covering Kubernetes clusters, virtual machines, cloud storage, and AI workloads in a single pane of glass.

~600 GCP Projects
59 GKE Clusters
2,222 Compute VMs
4,000+ Recommendations

The Problem

Managing cloud spend at enterprise scale is a visibility problem.

With hundreds of GCP projects, thousands of workloads, and multiple service types, waste hides everywhere: over-provisioned Kubernetes pods, idle VMs running 24/7, storage buckets nobody reads, and AI model calls that could use cheaper alternatives. The data existed in GCP’s APIs — but nobody was connecting it into a unified picture with actionable recommendations.

Each service type had its own console, its own mental model, and its own team responsible for it. There was no single place to answer “where is the waste across this organization, and what should we fix first?”

Scale

Metric Value
GCP Projects Monitored~600
GKE Clusters59
Compute Engine VMs2,222
Cloud Storage Buckets2,989
Right-Sizing Recommendations4,000+
API Endpoints123
Frontend Pages13
React Components61

Four Service Modules

Each module targets a different resource type with purpose-built analysis, scoring, and recommendations — all unified in a hub dashboard.

GKE — Kubernetes
59 clusters across ~600 projects
  • Collects CPU and memory metrics from every pod across all clusters
  • Calculates per-workload efficiency and classifies variability (Stable / Variable / Spiky)
  • Adaptive right-sizing headroom: 25% (stable) through 45% (spiky) above P95
  • Bulk YAML patch generation — preview before applying
  • Fleet-wide efficiency scoring with 7-day trend forecasting
  • Label compliance tracking for cost-allocation governance
GCE — Compute Engine
2,222 virtual machines
  • Idle VM detection — flags instances below 5% CPU utilization
  • Machine type downsizing recommendations
  • Spot instance conversion candidate scoring
  • Business-hours-only usage detection for auto-schedule recommendations
  • Committed Use Discount (CUD) coverage analysis with 1-year and 3-year savings projections
GCS — Cloud Storage
2,989 storage buckets
  • Abandoned bucket detection — no reads in 90+ days
  • Lifecycle policy JSON generation for storage class transitions
  • Public access risk flagging
  • Versioning waste tracking
  • Egress cost monitoring
AI — Vertex AI
Model spend from BigQuery billing
  • Model-level spend tracking from BigQuery billing export
  • Cheaper model alternative recommendations comparing cost-per-token
  • Anomaly detection — days exceeding 2× the rolling average
  • Spend breakdown by project for chargeback
Hub Dashboard

A four-quadrant executive summary aggregates cost, waste, and savings across all four services. Cross-service project detail pages combine every metric for a given GCP project into one view — answering “what are we spending on this project, and where’s the waste?” in seconds.

Conversational Layer — ARIA

Spotlight exposes its data to ARIA, the FinOps AI agent I built on top of this platform and the FinOps Reporting App. Eighteen of ARIA’s thirty tools read from Spotlight, and the app hosts both a full-page assistant and a chat widget in the navbar, backed by a reverse-proxy route that streams responses from the agent service. Users who don’t want to learn the filters can just ask.

Architecture

A full-stack web application with isolated per-service databases and a hybrid cost model layered over live GCP API data.

 React Frontend
TypeScript · Tailwind CSS · TanStack React Query · Recharts · 13 pages · 61 components
REST API (123 endpoints)
 FastAPI Backend
Python 3.11 · SQLAlchemy · Alembic · Automated twice-daily collection pipeline
 GKE Services
collector · metrics · cost engine
 GCE Services
collector · detection · rightsizing
 GCS Services
collector · lifecycle · detection
SQLite (WAL)
GKE database
SQLite (WAL)
GCE database
SQLite (WAL)
GCS database
 GCP APIs
Cloud Monitoring · Container API · Compute Engine API · Cloud Storage API · BigQuery Billing Export

Tech Stack

LayerTechnology
FrontendReact 18, TypeScript, Vite, TanStack React Query, Recharts, Tailwind CSS
BackendPython 3.11, FastAPI, SQLAlchemy, Alembic
Database3 SQLite databases with WAL mode (one per service, isolated write contention)
Data SourcesCloud Monitoring API, GKE Container API, Compute Engine API, Cloud Storage API, BigQuery Billing Export
AuthGCP Workload Identity (zero stored credentials)
DeploymentDocker multi-stage build, Kubernetes, ArgoCD
Infrastructure as CodeTerraform (IAM bindings across ~600 projects)

Key Technical Decisions

The “why” behind each major design choice — not just what was built, but the reasoning and trade-offs considered.

Getting accurate cloud cost data is surprisingly hard. I built a three-tier pricing engine that uses the best data available:

  • Actual billing data (preferred) — Queries the GCP BigQuery billing export for real invoiced costs, including Committed Use Discount and Sustained Use Discount credits. Cached with a 6-hour TTL.
  • Derived per-unit rates — When billing data exists, per-core and per-GB rates are derived from actual cost divided by capacity, reflecting real machine types, regions, and discount programs.
  • On-demand fallback — Machine type catalog covering 9 families (e2 through t2d) with regional multipliers, falling back to generic on-demand pricing when machine type is unknown.

The UI transparently labels every cost figure as “Billing” or “Estimated” so users know exactly what they’re looking at. This was critical for trust — FinOps decisions can’t be made against numbers nobody believes.

A flat “use P95 + 20% buffer” doesn’t work when some workloads spike 5× and others are perfectly flat. I classify every workload by its coefficient of variation and apply variable headroom:

Stability ClassVariability (CV)Headroom Applied
Stable< 25%25% above P95
Variable25 – 50%35% above P95
Spiky> 50%45% above P95

This prevents aggressive recommendations on unpredictable workloads while still capturing savings on stable ones. A 70% waste recovery rate further guards against over-optimization — the system recommends capturing most of the savings, not all of it, to leave a practical safety margin.

Each service module (GKE, GCE, GCS) gets its own SQLite database. This was a deliberate architectural choice:

  • Eliminates cross-service write contention — parallel collection runs don’t block each other
  • Independent schema evolution — Alembic migrations for GKE schema changes don’t touch GCE or GCS
  • Simplified backup and restore — a GCS database restore doesn’t require downtime for GKE data
  • Isolated failure domains — a corrupt GCE database doesn’t affect GKE metrics

Why not PostgreSQL? The workload is single-writer, read-heavy, and doesn’t require complex cross-service joins. SQLite with WAL mode handles it without the operational overhead of a separate database server.

  • Twice-daily schedule — runs automatically on a configurable cron, no manual trigger required
  • Parallel collection — all three services collect simultaneously using asyncio.gather
  • Concurrency lock — prevents overlapping runs if a collection takes longer than expected
  • 1-year retention policy — data older than 365 days is automatically pruned to control storage growth
  • Full audit trail — every collection run is logged with duration, resource counts, and error details

The audit trail was essential for trust: when a stakeholder asks “why does this cluster only have 30 days of data?” the answer is in the collection history, not a debugging session.

A dedicated correctness pass after the platform was in daily use. Every item below is a bug that only appears under conditions you don’t hit while developing — a pod dying mid-run, two requests racing, a resource being decommissioned:

  • Self-healing collection lock — the single-flight guard was a plain thread lock, which wedges permanently if the holder dies before releasing. Every subsequent collection would return 409 forever. It’s now a flag with a timestamp that auto-reclaims after two hours, longer than any plausible run.
  • Orphaned-run reconciliation — collection threads are killed abruptly on shutdown, leaving run records stuck “in progress” and status endpoints reporting a collection that would never finish. On startup, any run still marked in-progress necessarily predates this process, so it’s marked failed with an explicit “interrupted by restart” reason.
  • Staleness cutoff on “current” views — latest-snapshot queries now ignore metrics older than 48 hours, so decommissioned VMs and deleted pods stop appearing in the fleet as though they still exist.
  • Full natural keys in snapshot joins — grouping by resource name alone collides same-named resources across projects and zones. Queries now key on project + zone + instance, or project + cluster + namespace + pod.
  • Atomic cache rebind — the billing caches were refreshed by clearing and repopulating a dictionary, leaving a window where a concurrent reader saw half a cache. Refresh now builds the full result first and swaps the reference under a lock.
  • Read endpoints off the event loop — synchronous database work in async handlers was blocking the loop under load. Read routes are now plain synchronous functions that the framework runs in its threadpool; only genuinely 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.

This was a deliberate departure from the Node.js backend pattern used in other apps in the suite — driven by the problem domain:

  • GCP SDK maturity — Google’s Python client libraries (monitoring, container, bigquery, resource-manager) are first-class with rich typing and well-documented APIs
  • NumPy for statistical calculations — percentiles, linear regression, and coefficient of variation computed efficiently across thousands of workloads
  • FastAPI provides automatic OpenAPI docs, Pydantic validation, and async support out of the box
  • SQLAlchemy + Alembic — mature ORM with migration tooling, auto-initializing the schema on first startup

Trade-off accepted: Two languages across the broader suite (Python + Node.js) means no shared backend code — but each app uses the right tool for its specific problem.

  • No service account keys — Workload Identity maps the Kubernetes service account directly to a GCP IAM principal, eliminating key rotation and secret storage entirely
  • ~600 projects at scale — Terraform applies identical IAM bindings across all monitored projects in a single plan/apply cycle
  • Principle of least privilege — read-only roles only (container.viewer, monitoring.viewer, compute.viewer) plus a purpose-built custom role for storage
  • Custom role instead of storage.objectViewer — the stock role also grants storage.objects.get, meaning permission to read object contents across every monitored project. A FinOps tool only needs bucket metadata and IAM policy, so I replaced it with a custom role limited to buckets.list/get/getIamPolicy and objects.list — removing a large data-exfiltration surface that nothing in the app was using
  • Auditability — all permission grants are version-controlled in the Terraform module, reviewable in PRs
  • Reproducible — onboarding a new GCP project is a one-line Terraform variable addition

Features at a Glance

 Analysis & Recommendations
  • Fleet-wide right-sizing with P95/P99-based sizing and adaptive headroom
  • Bulk YAML patch generation with multi-select and preview
  • Idle VM detection with spot candidate scoring and schedule recommendations
  • GCS lifecycle optimization with downloadable lifecycle policy JSON
  • CUD coverage analysis with 1-year and 3-year savings projections
  • AI model alternative recommendations by cost-per-token
  • Label compliance tracking for cost-allocation governance
 Visualization & Exploration
  • 10+ interactive charts: efficiency distributions, fleet trends with 7-day forecast, spikiness scatter plots, cluster bubble charts
  • Workload drilldown modals with 30-day time series and interactive right-sizing calculator
  • Workload stability classification pages for GKE and GCE
  • Cross-service project detail pages combining all service metrics
  • Storage class breakdown charts and egress trend visualization
 UX & Operations
  • Global filter bars with cascading dropdowns (project, cluster, namespace, region, zone, storage class)
  • Dark mode with system preference detection and chart theme support
  • CSV export from every data table
  • Collection history audit trail per service
  • Methodology tooltips explaining every calculated metric
  • Lazy-loaded pages with code splitting (vendor, query, charts bundles)

Security Posture

Read-Only GCP Permissions
The application cannot modify any cloud resources. Viewer roles only across all APIs.
Workload Identity
Zero service account keys stored or managed. GCP credentials are never written to disk.
Non-Root Container
Runs as an unprivileged user (UID 1001). Minimal base image. No unnecessary packages.
VPN-Only Access
No public internet exposure. Access is restricted to the internal corporate network.
Metadata-Only Storage Access
A custom role replaces storage.objectViewer, removing the ability to read object contents across every monitored project.
Request ID Tracing
Structured JSON logging with correlation IDs across every request for full observability.

What I Learned

What Worked Well
  • Python + FastAPI was the right call — GCP client libraries, NumPy stats, and Alembic all native to the ecosystem
  • Adaptive headroom (25–45%) based on workload variability prevents one-size-fits-all sizing errors that erode stakeholder trust
  • Hybrid pricing model (BigQuery billing → derived rates → hardcoded fallback) handles partial billing data gracefully without silent failures
  • Workload stability classification (Stable / Variable / Spiky) gives users clear action paths, not just raw numbers
  • Bulk YAML patch generation bridges the gap between “identify waste” and “fix it” — the last mile of FinOps
  • Terraform IAM provisioning scales to ~600 projects with a single variable addition per new project
  • Three-database isolation eliminated cross-service write contention during parallel collection runs
Challenges
  • BigQuery billing export availability varies by project — fallback pricing logic added significant complexity to the cost engine
  • Cloud Monitoring API quotas required batching queries carefully across 59 clusters to avoid rate limiting
  • Reconciling billing export data with API-reported metrics — lag between resource changes and monitoring data availability was a persistent challenge
  • Designing a cost model flexible enough to handle CUD credits, sustained use discounts, and regional pricing variation across 9 machine families
  • Building the recommendation engine required balancing aggressiveness vs. safety across wildly different workload types
Would Do Differently
  • Add Autopilot cluster detection from day one to skip right-sizing recommendations where GKE already auto-optimizes
  • Implement namespace exclusion filters (kube-system, monitoring) from the start — retrofitting was tedious
  • Add automated recommendation tracking — capturing whether a user applied a recommendation and measuring the actual savings impact
  • Consider PostgreSQL early if multi-user write access or cross-service queries become requirements

The project reinforced that 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 wasn’t surfaced in a way that made action easy. The hardest problems weren’t technical; they were data problems: reconciling billing data with API metrics, handling monitoring lag, and designing a cost model flexible enough for real-world discount complexity.

 Back to Projects