A production, serverless GCP pipeline that ingests real-time METAR observations from 17 Florida stations, GOES-18 satellite lightning, upper-air radiosonde soundings, and NWS forecasts/alerts into BigQuery — then runs 6 purpose-trained ML models plus Gemini 1.5 Flash to produce 2-hour, venue-specific nowcasts for Central Florida theme parks. Published as an adaptive JSON API (5–15 min, 1 min during active storms) and consumed cross-cloud by an AWS Bedrock agent calling Azure Function tools.
4 external weather/lightning sources → 3 parallel Cloud Run ingestion jobs → BigQuery raw + curated views → spatial ML nowcasting + Gemini narratives → adaptive JSON publisher → GCS public bucket → cross-cloud consumers (GCP-native + an AWS Bedrock agent via Azure). Each layer is independently scalable and separately schedulable via Cloud Scheduler.
How METAR bytes, satellite lightning, and soundings become a 2-hour, venue-specific risk narrative.
weather-updater-v6 (every 10 min): fetches surface observations from aviationweather.gov for 17 Florida stations — temperature, dewpoint, pressure, wind, precipitation, visibility, present-wx codes.
weather-lightning-ingestor (every 10 min): pulls GOES-18 GLM flashes within 20 mi of the parks, 30-min lookback, MERGE-deduped into tbl_lightning_whisperer.
weather-updater-atmos (every 12 hr): fetches University of Wyoming radiosonde upper-air soundings (Station 72210, Tampa Bay) — CAPE, CIN, Lifted Index, K-Index, Total Totals, Showalter, precipitable water.
Raw observations land in partitioned/clustered tables. v_weather_current_enhanced adds enhanced thunderstorm detection (TS/VCTS/LTG pattern matching) with a clear-sky suppressor that prevents false HIGH alerts when METAR shows no activity. v_weather_forecast_enhanced gates CAPE/LI contribution so it only applies when precipitation or thunder is actually forecast.
6 scikit-learn models — retrained on-demand from 44K+ historical observations — score thunderstorm, precipitation, high-wind, fog, and venue-impact probability over a 2-hour horizon. BigQuery ST_DISTANCE detects nearby (100 mi) and upwind (150 mi) convective cells and boosts risk scores when they're moving toward a park.
weather-insights-generator calls Vertex AI Gemini 1.5 Flash to turn structured risk scores into human-readable operational narratives every 20 min, feeding vw_storm_state_current — a single-row, 5-state storm severity engine (CLEAR → SEVERE) fusing lightning + NWS alerts. weather-outage-tracker flags weather-attributed ride closures against ride_weather_sensitivity thresholds every 5 min.
weather-json-publisher serializes nowcasts, venue impact, insights, and station data to a public GCS bucket every 5–15 min — 1 min during active storms, via a self-toggled adaptive scheduler. Consumed directly by ride_whisperer and ops dashboards, and cross-cloud by an AWS Bedrock Claude agent calling Azure Function tools that read these same BigQuery views.
weather-ml-predictor runs 6 purpose-trained scikit-learn models — retrained on-demand by
weather-ml-trainer from 44K+ historical observations — to produce 2-hour, venue-specific
probability nowcasts. weather-insights-generator then calls Vertex AI Gemini 1.5 Flash to
turn each structured risk score into a human-readable operational narrative.
Thunderstorm probability, next 2 hrs, boosted by spatial storm-tracking (BigQuery ST_DISTANCE).
Precipitation probability, next 2 hrs, from current METAR + atmospheric instability indices.
25+ kt gust probability, next 2 hrs, from surface wind trend and pressure change.
Fog probability, next 2 hrs, from dewpoint depression and visibility trend.
Per-venue operational impact, joining live conditions against ride_weather_sensitivity.
High-impact venue closures — the highest-severity tier of the venue impact model.
| Model | ROC AUC | Accuracy | Predicts |
|---|---|---|---|
| thunderstorm_nowcast | 0.750 | 72% | Thunderstorm probability, next 2 hrs |
| precipitation_prediction | 0.990 | 99% | Precipitation probability, next 2 hrs |
| high_wind_prediction | 1.000 | 100% | 25+ kt gust probability, next 2 hrs |
| fog_prediction | 0.900 | 92% | Fog probability, next 2 hrs |
| venue_impact_prediction | 0.847 | 81% | Per-venue operational impact |
| high_impact_venue_prediction | 0.950 | 95% | High-impact venue closures |
The near-perfect wind/precipitation scores likely reflect strong signal correlation in METAR features (e.g. reported gusts directly implying "high wind") rather than a uniformly hard prediction problem — validated against a held-out temporal split, not a random split.
All raw tables use DAY partitioning on the time field and clustering on station_id — queries with time and station filters never scan more than the relevant partition slices.
| Field | Type | Description |
|---|---|---|
| station_id | STRING | ICAO station identifier (one of 17 Florida stations) |
| observation_time | TIMESTAMP | UTC observation time — partition key |
| temp_f / dewpoint_f | FLOAT64 | Temperature and dewpoint (°F) |
| relative_humidity | FLOAT64 | Relative humidity (%) |
| pressure_hpa | FLOAT64 | Station pressure (hPa) |
| wind_speed_kt / wind_gust_kt | FLOAT64 | Wind speed and gust (knots) |
| precip_rolling_3hr / 6hr | FLOAT64 | Rolling precipitation totals (derived) |
| pressure_change_1hr | FLOAT64 | 1-hour pressure trend (derived) |
| is_thunderstorm / is_fog / is_mist | BOOL | Condition flags from METAR present weather |
| adverse_heavy_rain / thunderstorm / fog / high_wind | BOOL | Adverse event flags for quick filtering |
| Field | Type | Description |
|---|---|---|
| cape_j_kg | FLOAT64 | Convective Available Potential Energy (J/kg) — thunderstorm potential |
| cin_j_kg | FLOAT64 | Convective Inhibition (J/kg) — cap strength |
| lifted_index | FLOAT64 | Lifted Index — negative = unstable, <-6 = severe |
| k_index | FLOAT64 | K-Index — thunderstorm probability (>40 = high) |
| total_totals_index | FLOAT64 | Total Totals — >55 indicates severe potential |
| showalter_index | FLOAT64 | Showalter Index — additional stability discriminator |
| pwat_inches | FLOAT64 | Precipitable water — flood potential |
| lcl / lfc / el pressure | FLOAT64 | Lifted condensation level, free convection level, equilibrium level (hPa) |
| thickness_1000_500_m | FLOAT64 | 1000-500mb thickness — rain vs snow discriminator |
| Field | Type | Description |
|---|---|---|
| forecast_timestamp | TIMESTAMP | Forecast valid time (UTC) — partition key |
| temperature_f / apparent_temperature_f | FLOAT64 | NWS temperature and feels-like (°F) |
| precipitation_intensity_mm_hr | FLOAT64 | Forecast precip rate (mm/hr) |
| cloud_cover_percent | FLOAT64 | Cloud cover (%) |
| weather_condition_category | STRING | NWS categorical condition string |
| active_alert_flag | BOOL | NWS active alert present for the forecast window — feeds vw_storm_state_current |
| Field | Type | Description |
|---|---|---|
| strike_id | STRING | Unique flash identifier |
| timestamp | TIMESTAMP | Flash detection time (UTC) |
| lat / lon | FLOAT64 | Flash location |
| energy_joules | FLOAT64 | Optical energy of the flash |
| nearest_park / distance_miles | STRING / FLOAT64 | Assigned at ingest — no runtime geo math. Filtered to within 20 mi of a park. |
vw_storm_state_current is a single-row, 5-state storm severity engine (CLEAR → WATCH → APPROACH → ACTIVE → SEVERE) fusing lightning proximity and NWS alerts — the primary go/no-go signal for outdoor ride operations. ride_weather_sensitivity stores historical storm/wind closure sensitivity percentages per individual ride, joined against live conditions by weather-ml-predictor and weather-outage-tracker. v_rides_currently_down joins rides currently down during operating hours with live weather context, and tbl_weather_ride_outage_log records weather-attributed closures written by the outage tracker.
Each curated view or model output is a stable, pre-joined query over the raw tables. The publisher serializes each to a structured JSON file at a predictable GCS path — web apps and cross-cloud agent tools fetch by path/query, no API layer needed.
2-hr rolling window with enhanced thunderstorm detection (TS/VCTS/LTG patterns) and risk scoring; clear-sky suppressor prevents false HIGH alerts.
48-hr NWS forecast with CAPE/LI contribution gated to only apply when precipitation or thunder is actually forecast.
Unified current + forecast view: overall_risk_score, risk_level, primary_concern, ride_impact_status, next-6-hour array.
Single-row, 5-state storm severity engine — the primary go/no-go signal for outdoor ride ops.
Historical storm/wind closure sensitivity percentages, per individual ride.
Rides currently down during operating hours joined with live weather context.
Deduplicated GLM flashes and 6-model probability nowcasts (thunderstorm, precip, wind, fog, venue impact) over a 2-hr horizon.
Weather-attributed ride closures written by weather-outage-tracker.
All compute runs as Cloud Run Jobs — no always-on instances, billed per execution. Cloud Scheduler drives the cadence; each job is independently deployable via Terraform.
Pulls and parses METAR observations for 17 Florida stations into BigQuery, deriving rolling precip, pressure change, and adverse condition booleans.
Ingests GOES-18 GLM lightning flashes within 20 mi of the parks (30-min lookback); MERGE-dedupes into tbl_lightning_whisperer.
Ingests Tampa Bay radiosonde upper-air soundings (Station 72210) — CAPE, CIN, Lifted Index, K-Index, and more.
Spatial storm-tracked nowcast + venue impact scoring using BigQuery ST_DISTANCE across all 6 production ML models.
Calls Vertex AI Gemini 1.5 Flash to generate atmospheric risk narratives and operational recommendations.
Flags weather-attributed ride closures against ride_weather_sensitivity risk thresholds.
Trains/retrains all 6 scikit-learn models from 44K+ historical observations; writes versioned pickled models to GCS.
Publishes the curated JSON API; manages the storm-adaptive fast-poll scheduler, escalating cadence automatically.
Every resource (BigQuery, GCS, Cloud Run Jobs, Cloud Scheduler, service accounts, IAM, Artifact Registry) is defined in Terraform with remote state in a versioned GCS backend. Deploy/validate/rollback/health-check scripts make the lifecycle repeatable, not a manual runbook.
bigquery.tf, cloud_run_jobs.tf, cloud_scheduler.tf, service_accounts.tf, storage.tf, with remote state in a versioned GCS backend. Image field intentionally ignore_changes so app deploys and infra deploys don't fight each other.
weather-ingestion-sa: BigQuery dataEditor + jobUser.
weather-publisher-sa: BigQuery dataViewer/dataEditor + GCS storage.objectAdmin (single bucket) + aiplatform.user (Gemini path only).
Read-only (dataViewer + jobUser) GCP service account whose key is stored in Azure Key Vault and fetched at runtime by the Azure Function tools — never checked in, never given write access.
Each job (ml, publisher, insights, ingestors) has its own Dockerfile + build.sh pushing to a private Artifact Registry repo.
A scripts/ folder ships pre-deploy validation, full apply sequence, post-deploy health checks (jobs, tables, models, scheduler state), and rollback — scripted and repeatable, not a manual runbook.
~$6/mo steady-state core pipeline at documented usage. The original full agentic stack (7 jobs, ~22,500 executions/mo) was ~$100–220/mo; a minimal hybrid tool-calling redesign cut that to ~$22–52/mo with no capability loss.
Key metrics, targets, and alert thresholds. Daily health check runs check_job_status.sh and queries BigQuery for data freshness.
| Metric | Target | Alert Threshold | Severity |
|---|---|---|---|
| Job success rate | > 99% | < 95% | P1 |
| METAR data freshness | < 15 min | > 30 min | P1 |
| Job execution duration | < 2 min | > 5 min | P2 |
| BigQuery storage | < 10 GB | > 50 GB | P3 |
| GCS object count | ~200 | < 50 or > 1000 | P2 |
| Monthly cost (core pipeline) | < $50 | > $250 | P2 |
| Model ROC AUC decay | < 5% drop | > 10% drop | P2 |
| HTTP 429 rate (external APIs) | < 1/hour | > 10/hour | P2 — exponential backoff active |
A working precursor to today's standardized agent-interoperability stacks, and a proven cost re-architecture story.
Calls weather "tools" implemented as Azure Function endpoints (/weather/current, /atmospheric, /trends, /summary) via an OpenAPI action-group schema — the agent decides when it needs current conditions, atmospheric indices, trends, or a region summary.
Authenticates to BigQuery using the read-only azure-bigquery-reader GCP service account, whose key lives in Azure Key Vault and is fetched at runtime — never checked in, never given write access.
Functionally the same shape as an MCP tool server or an OIDC-secured agent gateway, just implemented before those standards existed in this codebase — a natural modernization path (swap the static key for short-lived OIDC federation, move onto a standardized MCP tool server).
The original full agentic stack (7 Cloud Run jobs, ~22,500 executions/month, heavy BigQuery ML queries) was estimated at ~$100–220/month. A "minimal hybrid" redesign — one ingestion job plus tool-calling on read (~4,320 executions/month) — cut that to ~$22–52/month with no loss of capability, by moving compute from "always scheduled" to "on-demand at agent query time."
Thunderstorm, precipitation, high-wind, fog, venue-impact, and high-impact-venue prediction have different feature relevance and different cost-of-error profiles. Keeping them as 6 separately trained scikit-learn models (retrained on-demand by weather-ml-trainer from 44K+ observations, pickled and versioned in GCS) lets weather-ml-predictor report a distinct confidence per hazard rather than one blended score that obscures which risk is actually driving a recommendation. BigQuery remains the single source of truth for features and labels — the models just live outside it, where model choice and explainability are easier to control.
CAPE, CIN, and Lifted Index from upper-air soundings are leading indicators of convective development — they signal thunderstorm potential 3–6 hours before surface conditions deteriorate. METAR alone would only react to thunderstorms after they're already overhead. The atmospheric component of the ride impact score gives operations staff advance warning: a high CAPE + low CIN sounding at 12Z means the afternoon convective risk is elevated even if current surface conditions look fine. The University of Wyoming archives soundings at 00Z and 12Z — the 12-hour ingestion schedule matches this availability window exactly.
The consuming application is hosted on Azure, and now also an AWS Bedrock agent. A GCP API Gateway in front of BigQuery would add ~$50–100/month in API call costs and a round-trip to GCP on every request. Instead, the publisher job runs every 5–15 minutes (1 minute during storms) and writes pre-computed JSON to a CORS-enabled public GCS bucket. Azure web apps fetch the JSON directly — no GCP API calls at read time. GCS standard storage for ~200 objects costs under $1/month. This architecture decouples the compute (GCP) from the consumption (Azure, AWS) with no persistent connection required.
METAR observations accumulate at ~144 records/day per station (every 10 min). At 17 monitored stations, that's ~2,450 rows/day, ~894,000 rows/year — small in absolute terms, but the curated views are queried on every predictor/publisher run (every 10–20 min). Without partitioning, BigQuery would scan the full table on every query. DAY partitioning on observation_time with clustering on station_id means each view only scans the relevant partition and skips other stations via clustering — reducing per-query bytes scanned from GB to KB. BigQuery charges $5/TB scanned; partition + clustering drops the query cost to near zero.
Cloud Run Jobs are the right primitive for batch/scheduled workloads that run for seconds to minutes and then exit. Each ingestion job runs for <2 minutes every 15–60 minutes — an always-on container or Cloud Function with a long timeout would be idle >95% of the time. Cloud Run Jobs bill per CPU-second of execution, not per allocation time. Cloud Functions have a 9-minute max timeout — the atmospheric ingestion job parses complex sounding data and could approach this. Cloud Run Jobs have a 1-hour timeout with no per-invocation startup overhead once the container is cached. Cloud Scheduler triggers each job on its own schedule independently.