
I wanted a concrete GCP counterpart for each Databricks concept, one I had actually run rather than read about. gcp-lakehouse-mlops-lab builds the same lakehouse and MLOps workflow twice: as Databricks notebooks (Free Edition) and on GCP with Terraform, Dataproc Serverless, BigQuery, Cloud Run and Vertex AI. Two of six planned projects are built and verified. One caveat up front: the Databricks notebooks are written but were not executed here, because I had no workspace token. The GCP side is what ran and what I measured.
Project 1: a medallion lakehouse on Delta
One month of NYC yellow-taxi trips, 2.46M rows, taken through bronze, silver and gold on Delta Lake.
| Step | Databricks | GCP |
|---|---|---|
| Spark runtime | Serverless compute, spark exists | Dataproc Serverless batch, runtime 2.2, 2 x 4-core executors |
| Delta Lake | Built in | delta-spark and delta-storage jars staged in GCS, plus two Spark configs |
| Storage / catalog | Unity Catalog managed tables | Delta files in GCS, exposed as BigLake external tables (format = DELTA_LAKE) |
| Raw source | samples.nyctaxi.trips | EXPORT DATA from the public BigQuery taxi table |
What the run proved: bronze 2,463,900 rows, silver 2,291,506 after removing invalid and duplicate trips, gold 5,886 rows (day by pickup zone). A second run executes silver as a MERGE (Delta version 2) and the count stays identical, so it is idempotent. Time travel with versionAsOf = 0 returns the original snapshot and DESCRIBE HISTORY lists WRITE, OPTIMIZE, MERGE. Appending a frame with fare_amount as a string is rejected with DELTA_FAILED_TO_MERGE_FIELDS. And BigQuery reads the same files Spark wrote, with no copy and no load job.
Things that bit me
- Scala version. Runtime 2.2 is Spark 3.5 on Scala 2.13. The
delta-spark_2.12jar fails withNoClassDefFoundError: scala/Serializable; use_2.13. --jarsdoes not give you the Python API. With jars staged that way thedelta.tablesmodule is not on the path, so the job uses SQLMERGEandDESCRIBE HISTORY. On DatabricksDeltaTablejust works.TIMESTAMP_NTZ. BigQuery's Parquet export arrives timezone-less, which enables a Delta table feature BigQuery's reader may not support. The job casts to plaintimestampfirst.- No internet. Dataproc Serverless needs Private Google Access on the subnet and an internal-allow firewall rule.
Project 2: experiment tracking and registry
The task: predict whether a card-paid trip tips 20% or more, with features from the silver table read through BigQuery. The same 8-model sweep (logistic regression, random forest, hist-GB, XGBoost where available) is tracked three ways.
| Concern | Databricks | GCP |
|---|---|---|
| Tracking server | Built into the workspace | Self-hosted MLflow 3.17 on Cloud Run, private, IAM |
| Backend store | Managed | Cloud SQL Postgres (db-f1-micro) |
| Artifacts | Managed | GCS, proxied by the server (--serve-artifacts) so clients need no storage credentials |
| Auth | Workspace login or PAT | Cloud Run IAM, MLFLOW_TRACKING_TOKEN from an identity token |
| Registry | Unity Catalog | MLflow registry on the server |
| Managed alternative | n/a | Vertex AI Experiments (train_vertex.py), no server at all |
Metrics were identical on both trackers (same seed, same split). The best model was hist_gb with AUC about 0.641 and F1 about 0.887 on a 90k-row sample. The AUC is modest because tipping behaviour is noisy; the point here is the tooling, not the model. taxi-high-tip v1 is registered with the alias candidate, and the screenshot shows it next to the run that produced it.
Things that bit me
- Cloud Run returned 429s while logging models. A 1 GiB, 1-CPU instance was overwhelmed by large random-forest artifacts proxied through the server. 4 GiB and 2 CPU fixed it. The two failed sweeps (26 and 20 minutes) are still visible in the UI, which I think is the honest way to leave them.
- MLflow 3.x defaults sklearn serialization to
skops, which refuses tree types. Useserialization_format="cloudpickle"for your own models. --allowed-hosts: MLflow 3's middleware rejects unknown Host headers."*"is acceptable here only because Cloud Run IAM already gates every request.- Vertex AI Experiments quietly creates a TensorBoard instance outside Terraform.
down.shdeletes it. - The first Cloud Run deploy takes about 6 minutes because Alembic migrates a fresh Cloud SQL.
Cost and process
Project 1 takes about 10 minutes and a few cents. Project 2 adds Cloud SQL f1-micro at roughly 0.01 EUR an hour while it exists; Cloud Run scales to zero. Terraform is always applied from a saved, reviewed plan, never a blind -auto-approve, and down.sh destroys everything.
What is still planned
Model serving and promotion, a feature store with batch inference, governance with Dataplex and BigQuery policy tags, and an end-to-end capstone are listed in the repo as planned, not built.
Try it yourself
git clone https://github.com/soodrajesh/gcp-lakehouse-mlops-lab
cd gcp-lakehouse-mlops-lab
./scripts/p1-up.sh # ~10 min: Terraform, jars, raw export, Spark batch, BigQuery tables, checks
./scripts/p2-up.sh # ~10 min: Cloud SQL + MLflow on Cloud Run
gcloud run services proxy mlflow --region us-central1 --port 8099
./scripts/down.sh
📢 Have questions or feedback? Drop a comment below or connect with me on Twitter/X@spysood!