GCP Lesson 86 of 98

Production MLOps on Vertex AI: Building Reproducible Training and Deployment Pipelines

In a nutshell

Imagine two ways to build a car. In the first, one brilliant mechanic hand-assembles the whole thing in a garage. It runs — once. Nobody can build a second identical car, and if that mechanic leaves, the knowledge walks out with them. In the second, a factory assembly line: every station does one job, stamps a serial number on the part it makes, and passes it to the next station. Any car can be rebuilt from the blueprint, and if a defect shows up on the road, the serial numbers tell you exactly which parts and which blueprint version produced it.

A training script on a data scientist’s laptop is the garage. Vertex AI Pipelines is the assembly line. Each step (a “component”) runs in its own container, does one job, writes its output as a tracked file (an “artifact”) with a recorded lineage, and hands it to the next step. The whole line runs from a compiled blueprint (the “pipeline spec”) on a button-press, a code commit, or a schedule — and Vertex records the serial numbers so that months later you can answer “which data and which code produced the model serving this prediction?” without guessing.

That traceability is the entire point of MLOps, and it is why “just schedule my notebook” is not the same thing.

Level: Intermediate → Expert · Time: ~24 min

Laptop notebook Vertex AI Pipeline
Where it runs One Python process on your machine Isolated containers Vertex provisions and manages
Reproducible? “Works on my machine” Re-runs from a compiled spec + explicit parameters
How data passes between steps In-memory variables Typed artifacts written to Cloud Storage
Lineage In your head (or lost) Recorded automatically in ML Metadata
Re-run / schedule Manual, by hand CI trigger or the native pipeline scheduler
Who can run it Only you Anyone with the service account + the spec

Before this lesson, it helps to know: service accounts and least-privilege IAM (IAM fundamentals: roles, service accounts & policy), where training data usually lives (BigQuery deep dive), and the idea of a CI trigger on commit (Cloud Build & Cloud Deploy deep dive). For the bigger platform picture around this lesson, see Enterprise architecture: the GCP ML platform.

After this lesson you will be able to: author a typed KFP v2 pipeline and compile it to a spec; wire versioned datasets and Feature Store into training so runs are reproducible; gate model promotion behind an evaluation step and register immutable versions; canary a new model onto an endpoint with a traffic split and roll back in seconds; configure skew and drift monitoring; and trigger the whole thing from CI/CD and a schedule under a least-privilege service account.

Vertex AI Pipelines MLOps flow: CI compiles a pipeline spec, Vertex runs a managed DAG whose typed artifacts feed ML Metadata lineage, an evaluation gate promotes a version into the Model Registry, and a canary endpoint is watched by Model Monitoring

Read the diagram left → right: a commit compiles the pipeline to a spec in Cloud Storage, Vertex runs it as a managed DAG whose steps exchange typed artifacts (so ML Metadata captures full lineage), an evaluation gate decides whether the trained model is promoted into the Registry as a new version, and only a promoted version canaries onto an endpoint that Model Monitoring watches for skew and drift.

A training script that runs once on a data scientist’s laptop is not a model lifecycle. Production MLOps is the discipline of making every run reproducible, lineage-tracked, governed, and re-runnable from a commit hash — so that six months from now you can answer “which data and which code produced the model serving this prediction?” without guessing. This guide builds that on Vertex AI end to end: a typed Kubeflow pipeline, Feature Store and managed datasets wired into training, promotion through the Model Registry, canary deployment to an endpoint, drift monitoring, and the CI/CD plus IAM scaffolding that ties it together.

1. The architecture: KFP components, artifacts, and metadata lineage

Vertex AI Pipelines is a managed runner for pipelines authored with the Kubeflow Pipelines (KFP) v2 SDK. You compile a Python pipeline to a JSON spec; Vertex executes each step as an isolated container, captures inputs and outputs as typed artifacts, and records the whole graph in Vertex ML Metadata. That metadata store is the point of the whole exercise — it is what gives you lineage.

   commit / schedule
        |
        v
[KFP pipeline spec (compiled JSON in GCS)]
        |
        v
[Vertex AI Pipelines runner]  --records-->  [Vertex ML Metadata]
   |        |          |                          ^
   v        v          v                          |
[ingest]->[train]->[evaluate]->[register]---------+
   |        |          |            |
 Dataset  Model    Metrics    Model Registry version
(artifact)(artifact)(artifact)      |
                                    v
                          [Endpoint: canary -> 100%]

Two distinctions matter before you write code:

This distinction is the one beginners get wrong most often, so make it concrete:

Parameter Artifact
Examples max_depth=8, region="us-central1" Dataset, Model, Metrics
Passed By value (copied into the run) By reference (a Cloud Storage URI + metadata)
Typical size Small scalars and strings Files, sometimes large
Effect on lineage Recorded as a run input value Creates artifact-to-artifact edges in the graph
Declared as max_depth: int = 8 Output[Dataset] / Input[Model]

Pin your SDK. KFP v2 and the google-cloud-aiplatform SDK move quickly and the compiled spec format is versioned. Pin exact versions (for example kfp==2.* and a known-good google-cloud-aiplatform) in the build image and in CI so a compile today produces the same spec next quarter.

2. Author a typed pipeline with custom and prebuilt components

Start with one custom component. The @component decorator turns a function into a containerized step; base_image and packages_to_install define its environment. Inputs and outputs are declared with type annotations — Output[Dataset] hands you a path to write to, and Vertex registers the result as an artifact.

from kfp import dsl
from kfp.dsl import component, Input, Output, Dataset, Model, Metrics

@component(
    base_image="python:3.11-slim",
    packages_to_install=["pandas==2.2.2", "scikit-learn==1.5.1", "joblib==1.4.2"],
)
def train(
    training_data: Input[Dataset],
    model: Output[Model],
    metrics: Output[Metrics],
    max_depth: int = 8,
):
    import pandas as pd, joblib
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import cross_val_score

    df = pd.read_csv(training_data.path)
    X, y = df.drop(columns=["label"]), df["label"]

    clf = RandomForestClassifier(max_depth=max_depth, random_state=42)
    score = cross_val_score(clf, X, y, cv=5).mean()
    clf.fit(X, y)

    # Write to the artifact path Vertex provisioned, then log lineage metadata.
    joblib.dump(clf, model.path)
    model.metadata["framework"] = "sklearn"
    metrics.log_metric("cv_accuracy", float(score))

Compose components into a pipeline with @dsl.pipeline. Passing task_a.outputs["x"] into task_b is what creates the dependency edge and the lineage link — never shuttle data through side channels.

@dsl.pipeline(
    name="fraud-training",
    pipeline_root="gs://acme-mlops-artifacts/pipeline-root",
)
def training_pipeline(project: str, region: str, max_depth: int = 8):
    ingest_task = ingest(project=project, region=region)
    train_task = train(
        training_data=ingest_task.outputs["training_data"],
        max_depth=max_depth,
    )
    evaluate(
        model=train_task.outputs["model"],
        metrics=train_task.outputs["metrics"],
    )

Compile to a spec. The compiled JSON is the deployable artifact — treat it like a build output, not source.

python -c "from kfp import compiler; import pipeline; \
compiler.Compiler().compile(pipeline.training_pipeline, 'fraud_training.json')"

For heavy lifting, lean on Google Cloud Pipeline Components (GCPC) rather than rolling your own — pip install google-cloud-pipeline-components. It ships first-party, supported ops such as CustomTrainingJobOp (runs a training container as a Vertex Custom Job, which is how you get GPUs and distributed training), ModelUploadOp, EndpointCreateOp, and ModelDeployOp. Prefer these for any step that touches a Vertex resource; they are maintained against API changes and emit the correct metadata.

The two component styles cover different needs — reach for the simpler one until you can’t:

Lightweight Python component Container component
Defined by @dsl.component on a plain function A prebuilt image + entrypoint command
Environment base_image + packages_to_install Whatever the image already ships
Best for Glue logic, evaluation, small dependencies Heavy/custom runtimes, existing training images
Build step Packaged for you at compile time You build and push the image yourself
Example the train function above a CUDA image, or a GCPC op like ModelUploadOp

3. Wire Feature Store and managed datasets into training

Reproducibility dies the moment training reads from a mutable source. Two Vertex primitives fix this.

Managed datasets give a stable, versioned handle to training data. Reference one in the pipeline and Vertex records the dataset resource as lineage. Create it from BigQuery or GCS:

gcloud ai datasets create \
  --display-name="fraud-training-v3" \
  --metadata-schema-uri="gs://google-cloud-aiplatform/schema/dataset/metadata/tabular_1.0.0.yaml" \
  --region=us-central1

Feature Store solves the harder problem: training-serving skew caused by computing features differently in batch and online paths. The current generation, Vertex AI Feature Store, serves features directly from a BigQuery source registered as a feature view. You define the source once; offline training reads point-in-time-correct values and online serving reads the same definitions, so a feature means the same thing in both places.

# Online store that backs low-latency serving
gcloud ai feature-online-stores create fraud_online_store \
  --region=us-central1 \
  --bigtable-min-node-count=1 \
  --bigtable-max-node-count=3 \
  --bigtable-cpu-utilization-target=70

# A feature view mapping a BigQuery source into that store
gcloud ai feature-views create txn_features \
  --feature-online-store=fraud_online_store \
  --region=us-central1 \
  --big-query-source-uri="bq://acme.features.txn_features" \
  --entity-id-columns=account_id \
  --sync-config-cron="0 * * * *"

In the pipeline, an ingest component fetches the offline feature values for the training entities, writing them as an Output[Dataset] so the exact slice is captured. The principle: training data flows in as a tracked artifact, never a raw query embedded in code.

4. Promote artifacts through the Model Registry with versioning

A trained model that lives only as a file in a bucket has no governance story. Upload it to the Vertex AI Model Registry, which gives a stable resource name, immutable version IDs, and aliases (such as default or champion) that you can repoint without changing serving config.

The clean pattern is a register step in the pipeline using ModelUploadOp. Crucially, set parent_model so a new upload becomes the next version of an existing model rather than a brand-new model — this is what builds a version history you can audit and roll back across.

from google_cloud_pipeline_components.v1.model import ModelUploadOp

ModelUploadOp(
    project=project,
    location=region,
    display_name="fraud-detector",
    parent_model="projects/acme/locations/us-central1/models/1234567890",
    unmanaged_container_model=train_task.outputs["model"],
    serving_container_image_uri=(
        "us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-5:latest"
    ),
)

Gate the upload behind your evaluate step. In KFP, return a boolean from evaluation and branch with dsl.If so a model that fails the accuracy or skew bar never reaches the registry. The registry is a promotion boundary, not a dumping ground.

Concept What it is Use it for
Model Stable parent resource The logical model (“fraud-detector”)
Version Immutable child of a Model Each promoted training run
Alias Movable pointer to a version champion, default, fast rollback

Inspect history from the CLI before any deploy:

gcloud ai models list --region=us-central1
gcloud ai models describe MODEL_ID --region=us-central1

5. Deploy to endpoints: traffic splitting, canaries, and autoscaling

A Vertex Endpoint is the serving surface; you deploy one or more model versions behind it and split traffic by percentage. This is the mechanism for canaries: deploy the new version at 10 percent, watch monitoring, then shift to 100.

Create the endpoint, then deploy the candidate alongside the incumbent with a traffic split:

gcloud ai endpoints create \
  --display-name="fraud-detector-ep" \
  --region=us-central1

# Deploy candidate at 10%, leave 90% on the existing deployed model.
gcloud ai endpoints deploy-model ENDPOINT_ID \
  --region=us-central1 \
  --model=MODEL_ID \
  --display-name="fraud-v3-canary" \
  --machine-type=n1-standard-4 \
  --min-replica-count=2 \
  --max-replica-count=10 \
  --traffic-split=0=90,NEW_DEPLOYED_MODEL=10

Key points that bite people:

gcloud ai endpoints update ENDPOINT_ID \
  --region=us-central1 \
  --traffic-split=NEW_DEPLOYED_MODEL=100

Rollback is the same command pointed back at the old deployed model ID. Because both versions stay deployed during the canary window, rollback is seconds, not a rebuild.

Online endpoints are not the only way to serve. When you need to score a whole table on a schedule rather than answer live requests, use batch prediction — and the economics are completely different:

Online prediction Batch prediction
Shape Endpoint + deployed model, always-on replicas A one-off BatchPredictionJob
Latency Milliseconds, synchronous request Minutes to hours, asynchronous
Scaling Between min/max replicas; no scale-to-zero Spins up, runs, tears down
Cost profile Pays 24/7 for the min replicas Pays only for the job’s duration
Input / output JSON instances in the request BigQuery ↔ BigQuery, or GCS files
Reach for it when Low-latency, per-request serving Scoring a full table nightly/weekly

6. Model Monitoring for training-serving skew and feature drift

A model that was accurate at deploy time silently rots as production data shifts. Vertex AI Model Monitoring samples prediction requests, compares feature distributions against a baseline, and alerts when they diverge.

Two signals to configure:

You attach a monitoring job to the endpoint, point it at the training data as baseline, set per-feature thresholds (a distance score above which an alert fires), and set a sampling rate so you monitor a representative fraction without paying to log every request. Wire alerts to a notification channel and triage on a threshold breach — a fired skew alert is a strong prior that retraining is due.

Pick thresholds empirically. Start with the platform defaults, watch the distance scores for a week, then tighten. Thresholds set too tight produce alert fatigue; too loose and drift is real before anyone notices.

7. Trigger pipelines from CI/CD and Cloud Scheduler with parameterization

A pipeline you launch by hand from a notebook is not production. Two triggers cover the real cases.

CI/CD on commit. A merge to main should compile the pipeline, push the spec to GCS, and submit a run. The submit step is a few lines with the SDK; parameter_values is where reproducibility lives — every knob is an explicit input, never a default buried in code.

from google.cloud import aiplatform

aiplatform.init(project="acme", location="us-central1")

job = aiplatform.PipelineJob(
    display_name="fraud-training",
    template_path="gs://acme-mlops-artifacts/specs/fraud_training.json",
    pipeline_root="gs://acme-mlops-artifacts/pipeline-root",
    parameter_values={"project": "acme", "region": "us-central1", "max_depth": 8},
    enable_caching=True,
)
job.submit(service_account="vertex-pipelines@acme.iam.gserviceaccount.com")

In Cloud Build, run that submit inside a step. Note submit() is non-blocking, which is what you want in CI — fire the run and let Vertex own the long-running execution rather than holding a build minute open for an hour.

steps:
  - name: "python:3.11"
    entrypoint: bash
    args:
      - -c
      - |
        pip install -q kfp==2.* google-cloud-aiplatform
        python compile_pipeline.py
        gsutil cp fraud_training.json gs://acme-mlops-artifacts/specs/
        python submit_pipeline.py
options:
  logging: CLOUD_LOGGING_ONLY

Scheduled retraining. For periodic retraining, use the native pipeline scheduler rather than a hand-rolled cron job — it manages the recurring run and reuses the same parameterized template.

schedule = job.create_schedule(
    cron="0 3 * * 1",            # 03:00 every Monday
    display_name="weekly-fraud-retrain",
    max_concurrent_run_count=1,
    service_account="vertex-pipelines@acme.iam.gserviceaccount.com",
)

enable_caching=True skips steps whose inputs are unchanged, which saves real money on reruns. Turn it off for the scheduled retrain — fresh data is the entire point, and a cache hit on ingest would defeat it.

8. Governance: IAM on runs, cost controls, and reproducibility guarantees

Three controls turn a working pipeline into a governed one.

IAM, least privilege. Pipeline steps execute as a dedicated service account, not the default Compute Engine SA. Create one and grant only what the pipeline touches:

gcloud iam service-accounts create vertex-pipelines \
  --display-name="Vertex Pipelines runtime"

gcloud projects add-iam-policy-binding acme \
  --member="serviceAccount:vertex-pipelines@acme.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"

The runtime SA also needs object access on the pipeline-root bucket and read access on data sources (BigQuery, Feature Store). Grant those scoped to the specific bucket and dataset, not project-wide. Humans submitting runs need roles/aiplatform.user; reserve admin roles for the platform team.

Cost controls. The recurring spend is endpoint replicas (running 24/7) and training compute. Right-size min-replica-count, label every Vertex resource for cost attribution, and set a billing budget with alerts so a runaway autoscale or a forgotten endpoint surfaces before the invoice does.

Reproducibility guarantees. This is the payoff. With the pieces above, every production model satisfies: the code is a compiled spec from a commit; the data is a versioned managed dataset and point-in-time Feature Store reads; the parameters are explicit parameter_values; the environment is pinned base images; and the lineage is recorded in ML Metadata linking the serving model version back through training to the exact input artifacts. That chain is what lets you answer the audit question instead of shrugging at it.

Going deeper

Everything above is the working shape. This section is the machinery underneath it — the parts that decide whether the pipeline scales, stays secure, and behaves the way you expect on the edge cases.

How the execution cache actually keys

Caching is not “did this run before?” — it is a content hash. For each task, Vertex computes a cache key from the component specification (the container image, command, and packaged code), the input artifact URIs, and the parameter values. If a task with the same key already ran successfully under the same pipeline_root in the same project, Vertex reuses that prior output artifact instead of executing. This is why an ingest step with byte-identical parameters returns weeks-old data: the key never changed, even though the underlying table did. You can override per task rather than globally:

train_task = train(training_data=ingest_task.outputs["training_data"], max_depth=8)
train_task.set_caching_options(False)   # force this step to always execute

The mental correction beginners need: the cache keys off declared inputs, not data freshness. A component that reads a mutable source must encode time (a snapshot date, a table decorator) as an explicit input, or the cache will happily serve the past.

The compiled spec is versioned IR

KFP v2 compiles your pipeline to an intermediate representation (IR) — YAML by default (.yaml), though JSON is equivalent and accepted. The IR carries a schemaVersion and the sdkVersion that produced it, and the Vertex runner validates it against the range it supports. That is the concrete reason to pin kfp: an IR compiled by a much newer or older SDK can be rejected or behave differently. TFX pipelines reach the same runner through the KFP DAG runner, so “Vertex AI Pipelines” is really “run this IR,” whichever SDK authored it.

Artifacts, URIs, and the Metadata graph you can query

An artifact is two things bound together: a file under pipeline_root in Cloud Storage (its uri) and a typed node in Vertex ML Metadata. Inside a component, model.path is a local mount of model.uri; you write to the path, Vertex persists the URI. The Metadata store is a real graph — Artifact, Execution, and Context nodes joined by events — and it is queryable, not just a picture in the console:

from google.cloud import aiplatform
aiplatform.init(project="acme", location="us-central1")

# Every Model artifact this project has produced, newest first.
for a in aiplatform.Artifact.list(filter='schema_title="system.Model"'):
    print(a.display_name, a.uri, a.metadata)

Traversing those edges programmatically is how you build “what fed this model?” tooling instead of clicking through the lineage tab.

Real training runs in a Custom Job, not the pipeline node

A @component executes on a single, modest pipeline node — fine for glue and evaluation, wrong for serious training. Production training goes through a Custom Job: CustomTrainingJobOp, or wrap your component with create_custom_training_job_from_component, which launches a separate Vertex training job with its own worker pool — GPUs or TPUs, multiple workers, an optional reduction server for all-reduce gradient aggregation. The pipeline step just submits and waits.

from google_cloud_pipeline_components.v1.custom_job import (
    create_custom_training_job_from_component,
)

gpu_train = create_custom_training_job_from_component(
    train,                       # your @component
    machine_type="n1-standard-8",
    accelerator_type="NVIDIA_TESLA_T4",
    accelerator_count=1,
    replica_count=1,
)
# use gpu_train(...) in the pipeline exactly where you used train(...)

Parallelism and fan-out

The DAG runs independent tasks in parallel automatically. To fan out over a list — one model per region, a hyperparameter sweep — use dsl.ParallelFor with an optional parallelism cap so you don’t blow through accelerator quota, and dsl.Collected to gather the outputs back:

with dsl.ParallelFor(items=regions, parallelism=3) as region:
    t = train_region(region=region)

Monitoring v2, decoupled from the endpoint

The endpoint-attached monitoring job in section 6 is the original (v1) shape. The current approach is a standalone model monitor resource (Model Monitoring v2): configured once against a model, it covers both online endpoints and batch prediction, runs on its own schedule, and computes skew and drift without being welded to a single endpoint. New work should prefer the model-monitor resource; the endpoint-attached job remains for existing setups. The two signals — skew against the training baseline, drift against a past serving window — mean the same thing in both.

Security and isolation for regulated workloads

Three controls matter when the data is sensitive. Private Service Connect makes an endpoint reachable only inside your VPC, never the public internet. VPC Service Controls wrap Vertex, Cloud Storage, and BigQuery in a perimeter, so a compromised component cannot exfiltrate artifacts to an outside project. CMEK (customer-managed encryption keys) on datasets, models, and endpoints puts you in control of the encryption keys and their rotation. Combined with the least-privilege runtime SA from section 8, a pipeline step can touch exactly its inputs and outputs and nothing else.

Cost and quota shape at scale

The pipeline orchestration itself is cheap — a small per-run fee plus the compute each step actually consumes. The real money is elsewhere: Custom Job accelerators while training and endpoint replicas billing 24/7 for the min-replica-count you set. The limits that bite at scale are regional: accelerator quota for custom training and the replica quota for endpoints. Fan-out with ParallelFor can exhaust accelerator quota instantly, which is exactly what the parallelism cap is for. Tie every run to Vertex AI Experiments (aiplatform.start_run(...), autologging) so you can compare metrics and cost across runs rather than eyeballing logs.

Enterprise scenario

A fraud team’s weekly retrain started shipping models that passed every offline gate but degraded live precision within hours. The lineage graph looked clean — versioned dataset, point-in-time Feature Store reads, the works. The actual cause: their scheduled run had inherited enable_caching=True from the CI submit script. Cloud Scheduler fired every Monday, but the ingest component’s inputs (project, region, the BigQuery view URI) were byte-identical week over week, so Vertex served a cached ingest artifact from weeks earlier. Training ran on stale data while the feature view kept syncing fresh rows for serving — a self-inflicted training-serving skew that monitoring eventually flagged, but only after the bad version took traffic.

The fix was twofold. First, caching off on the schedule, non-negotiable:

job = aiplatform.PipelineJob(
    display_name="weekly-fraud-retrain",
    template_path="gs://acme-mlops-artifacts/specs/fraud_training.json",
    pipeline_root="gs://acme-mlops-artifacts/pipeline-root",
    parameter_values={"project": "acme", "region": "us-central1", "max_depth": 8},
    enable_caching=False,   # fresh data is the entire point of a retrain
)
job.create_schedule(
    cron="0 3 * * 1",
    max_concurrent_run_count=1,
    service_account="vertex-pipelines@acme.iam.gserviceaccount.com",
)

Second, they made the ingest input change on purpose: the component now takes a data_snapshot_date parameter bound to the run date, so identical-input cache hits are impossible by construction even if someone re-enables caching. The lesson the platform team wrote into their runbook: caching keys off declared inputs, not data freshness — a pipeline that reads a mutable source must encode time as an explicit parameter or it will silently train on the past.

Practice challenges

Work these in order; each builds on the last. Solutions are collapsed — try first, then check.

1. (Beginner) Compile a one-component pipeline and inspect the spec. Author a trivial pipeline (a single @dsl.component that logs a metric), compile it, and confirm what the compiled file actually is.

<details> <summary>Solution</summary>

from kfp import compiler
compiler.Compiler().compile(my_pipeline, "pipeline.yaml")

Open pipeline.yaml and find schemaVersion and sdkVersion near the top. Why: the spec is a versioned build artifact, not source — it is what you deploy and re-run, and its version is why you pin kfp. </details>

2. (Beginner) Submit a run with explicit parameters and a service account. Submit the compiled spec as a PipelineJob, passing every knob through parameter_values and running under a named SA, not the default.

<details> <summary>Solution</summary>

job = aiplatform.PipelineJob(
    display_name="demo",
    template_path="gs://.../pipeline.yaml",
    pipeline_root="gs://.../root",
    parameter_values={"project": "acme", "region": "us-central1", "max_depth": 8},
)
job.submit(service_account="vertex-pipelines@acme.iam.gserviceaccount.com")

Why: parameters carry reproducibility (nothing hidden as a code default) and the explicit SA carries least privilege. </details>

3. (Intermediate) Gate registration behind an evaluation step. Make the pipeline register the model only when a validation metric clears a threshold.

<details> <summary>Solution</summary>

Return a boolean from the evaluate component (e.g. Output named deploy), then branch:

eval_task = evaluate(model=train_task.outputs["model"], threshold=0.90)
with dsl.If(eval_task.outputs["deploy"] == True, name="promote"):
    ModelUploadOp(..., unmanaged_container_model=train_task.outputs["model"])

Why: the Model Registry is a promotion boundary — a model that fails the bar must never reach it. </details>

4. (Intermediate) Version a model and move the champion alias. Upload a new version under an existing model, then point champion at it without redeploying anything.

<details> <summary>Solution</summary>

Register with parent_model set to the existing model’s resource name (that makes it the next version), then move the alias with the SDK’s registry:

from google.cloud import aiplatform
registry = aiplatform.models.ModelRegistry(model="projects/acme/locations/us-central1/models/1234567890")
registry.add_version_aliases(new_aliases=["champion"], version="2")
registry.remove_version_aliases(target_aliases=["champion"], version="1")

Why: aliases decouple “which version is champion” from serving config — repointing is instant and reversible, and it is your rollback lever. </details>

5. (Advanced) Turn a training component into a GPU Custom Job. Your train @component outgrew a single pipeline node. Run it as a Custom Job with a T4 without rewriting the function.

<details> <summary>Solution</summary>

from google_cloud_pipeline_components.v1.custom_job import (
    create_custom_training_job_from_component,
)
gpu_train = create_custom_training_job_from_component(
    train, machine_type="n1-standard-8",
    accelerator_type="NVIDIA_TESLA_T4", accelerator_count=1,
)
# call gpu_train(...) in the pipeline where train(...) used to be

Why: a @component is single-node; a Custom Job gives you accelerators, multiple workers, and distribution while the pipeline step just orchestrates. </details>

6. (Advanced) Schedule a weekly retrain that cannot hit a stale cache. Set up a Monday-morning retrain where a cache hit on ingest is impossible by construction — even if someone re-enables caching later.

<details> <summary>Solution</summary>

Disable caching and bind a run-dated parameter into the mutable-source component:

import datetime as dt
job = aiplatform.PipelineJob(
    display_name="weekly-retrain",
    template_path="gs://.../pipeline.yaml",
    pipeline_root="gs://.../root",
    parameter_values={"project": "acme", "region": "us-central1",
                      "data_snapshot_date": dt.date.today().isoformat()},
    enable_caching=False,
)
job.create_schedule(cron="0 3 * * 1", max_concurrent_run_count=1,
                    service_account="vertex-pipelines@acme.iam.gserviceaccount.com")

Why: the cache keys off declared inputs — a changing data_snapshot_date makes identical-input hits impossible, so freshness is guaranteed by the spec, not by remembering to flip a flag. </details>

Common beginner mistakes

These are conceptual traps — the wrong mental model, not just a wrong flag. Fix the model and the flags follow.

Verify

Confirm each layer is actually wired, not just declared.

# Pipeline run reached completion
gcloud ai pipeline-jobs describe PIPELINE_JOB_ID --region=us-central1 \
  --format="value(state)"
# expect: PIPELINE_STATE_SUCCEEDED

# A new model version was registered
gcloud ai models list --region=us-central1 --filter="displayName=fraud-detector"

# Endpoint shows the expected traffic split
gcloud ai endpoints describe ENDPOINT_ID --region=us-central1 \
  --format="yaml(trafficSplit, deployedModels[].id)"

# Online prediction returns
gcloud ai endpoints predict ENDPOINT_ID --region=us-central1 \
  --json-request=instances.json

In the console, open the pipeline run and confirm the lineage graph links the dataset artifact, the model artifact, and the registered version — if the edges are missing, you passed data through a side channel instead of as a typed output.

Production checklist

Pitfalls

Glossary

Vertex AIMLOpsKubeflowGCPCI/CD
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments