Why Most Models Never Leave the Notebook
A student in our spring 2025 MLOps cohort had a gradient-boosted classifier sitting in a Jupyter notebook that genuinely worked. Solid validation AUC, clean feature pipeline, reasonable inference time. It had been sitting there for six weeks because nobody on the team knew how to move it. Not unusual. Getting a model into Google Vertex AI and actually serving predictions is a completely different skill set from building the model, and most ML courses skip it entirely.
This post walks through the actual steps. Real code, real tool names, real decisions you'll have to make.
What a Google Vertex AI Pipeline Actually Looks Like
A Vertex AI Pipeline is a directed acyclic graph where each node is a containerized function. You define components with the KFP SDK v2, compile them into a YAML artifact, and submit that artifact to Vertex Pipelines to run on managed compute. The pipeline runner handles scheduling, retries, logging, and artifact tracking.
The mental model shift matters more than any specific API call. Your notebook is a linear script. A pipeline is a graph of isolated steps, each with its own container, its own inputs, its own outputs, and its own failure surface. Data flows between steps as typed artifacts (Dataset, Model, Metrics) rather than Python variables sitting in memory.
Here's a minimal component definition:
from kfp.v2.dsl import component, Dataset, Output
@component(
base_image="us-docker.pkg.dev/vertex-ai/training/scikit-learn-cpu.1-0:latest",
packages_to_install=["pandas==2.1.4", "google-cloud-storage==2.14.0"]
)
def preprocess_data(
raw_data_uri: str,
processed_dataset: Output[Dataset]
) -> None:
import pandas as pd
from google.cloud import storage
# fetch, clean, save
df = pd.read_csv(raw_data_uri)
df = df.dropna()
df.to_csv(processed_dataset.path, index=False)
That decorator is doing a lot. It pins the base image, installs extra packages at runtime, and registers the output as a typed artifact that downstream components can consume. Get the base image wrong and you will spend an hour debugging import errors inside a container you can't easily inspect. Pick an image from us-docker.pkg.dev/vertex-ai/training/ that matches your framework first; add extras with packages_to_install second.
The Containerization Step Nobody Warns You About
This is where notebook-to-production ML breaks down most often. In a notebook, your environment is whatever conda or pip has installed. In a Vertex component, you get a fresh container on every run. Nothing carries over.
I've watched three separate teams hit the same wall in a single cohort. Each one lost half a day to a different flavor of the same root cause.
The first flavor: implicit dependencies. Your notebook calls a utility function from utils.py sitting in the same directory. The container doesn't have that file. You need to either inline the function, package it as a library, or copy it into a custom Docker image.
The second: credential handling. pd.read_csv("gs://my-bucket/data.csv") works on your laptop because you ran gcloud auth application-default login. Inside a Vertex component, authentication runs through the pipeline's service account. Make sure that account has Storage Object Viewer on your bucket before you run anything. Sounds obvious. Costs an hour if you skip it.
The third: package version pinning. The base images ship with specific library versions, and adding scikit-learn==1.4.0 via packages_to_install when the base image already has 1.0.2 can produce silent conflicts that only surface during inference. Pin everything explicitly.
For projects past the prototype stage, we push teams toward custom base images stored in Artifact Registry. More upfront work, zero surprise dependency failures at runtime. Worth it every time.
How to Wire Components Into a Full Pipeline
Once you have components, you compose them into a pipeline function and compile it:
from kfp.v2 import dsl, compiler
@dsl.pipeline(
name="churn-prediction-pipeline",
description="Preprocess, train, evaluate, register"
)
def churn_pipeline(raw_data_uri: str, project: str, location: str):
preprocess_task = preprocess_data(raw_data_uri=raw_data_uri)
train_task = train_model(
dataset=preprocess_task.outputs["processed_dataset"]
)
evaluate_task = evaluate_model(
model=train_task.outputs["model"],
dataset=preprocess_task.outputs["processed_dataset"]
)
_ = register_model(
model=train_task.outputs["model"],
metrics=evaluate_task.outputs["metrics"],
project=project,
location=location
)
compiler.Compiler().compile(
pipeline_func=churn_pipeline,
package_path="churn_pipeline.yaml"
)
That YAML file is the artifact you submit to Vertex. You can run it from the console, from the Python SDK, or from a Cloud Build trigger. We wire it to a Cloud Build trigger so every push to main kicks off a new pipeline run. That's the moment this starts feeling like real MLOps rather than a fancy notebook runner.
What Happens After the Model Is Registered?
Vertex Model Registry stores the artifact, the serving container, and the lineage back to the pipeline run that produced it. From there you have two paths: Vertex Endpoints for real-time serving, or Vertex Batch Prediction for offline scoring.
For real-time, deploying to an endpoint looks like this:
from google.cloud import aiplatform
aiplatform.init(project="your-project", location="us-central1")
model = aiplatform.Model(model_name="projects/.../models/churn-model")
endpoint = model.deploy(
machine_type="n1-standard-4",
min_replica_count=1,
max_replica_count=5,
traffic_split={"0": 100}
)
Swapping from n1-standard-4 to an a2-highgpu-1g node for a heavier model is one argument change. The application calling the endpoint never knows. That portability is real, and it's one of the things that makes Vertex worth the setup cost compared to self-managed Kubernetes serving. I've seen teams spend three weeks tuning a Kubernetes ingress that Vertex would have handled in an afternoon.
For monitoring, add Vertex Model Monitoring to that endpoint and it will alert you when feature distributions drift past a configurable threshold. Beats writing your own drift detector from scratch and maintaining it forever.
Where to Go From Here
We cover this full workflow, including custom training containers, Vertex Experiments for hyperparameter tracking, and CI/CD integration with Cloud Build, in our MLOps & AI Infrastructure course. The MLOps Deployment lab environment is where students run these pipelines against real GCS buckets and live Vertex Endpoints rather than simulated data. That distinction matters: simulated data hides the IAM errors and network timeouts that real pipelines surface immediately.
The honest question to ask yourself right now: does your current model have a requirements.txt, a defined entry point, and a service account with the right IAM roles? If not, start there. Everything else in Google Vertex AI is solvable once your fundamentals are locked.