๐ โ ๐งน โ ๐๏ธ โ ๐ ย ย (automated with โ๏ธ Vertex AI Pipelines)
The goal of this tutorial is to take the pieces of the Cheese App we have built separately (collecting images, cleaning them, training a model, deploying it) and connect them into one automated ML pipeline that runs in the cloud with a single command.
So far we ran each step by hand: start a container, run a command, check the bucket, start the next container. That works once, but it is slow, easy to get wrong and hard to repeat. A workflow manager runs the steps for us, in the right order, on cloud machines, and records what happened in each run. Here we use Vertex AI Pipelines (built on Kubeflow Pipelines, kfp).
Note on names: In 2026 Google renamed Vertex AI to Gemini Enterprise Agent Platform. The console now says "Agent Platform", but the API (
aiplatform.googleapis.com), the Python SDKs (google-cloud-aiplatform,kfp), Pipelines, Training, Model Registry and Endpoints work exactly as before. In this tutorial, "Vertex AI" and "Agent Platform" mean the same thing.
Here is the story, step by step:
- Data Collector downloads cheese images from Bing image search for each label (brie, gouda, gruyere, parmigiano), zips them and uploads
raw.zipto our bucket. - Data Processor downloads
raw.zip, removes duplicate and invalid images, and uploadsclean.zip. - Model Training submits a training job to Vertex AI. The trained model, with a serving function attached, is saved to
gs://<bucket>/model. - Model Deploy uploads that model to the Model Registry and deploys it to an Endpoint.
- First we run each step on its own, inside its own container, to make sure it works.
- Then we publish the steps where Google can reach them: the data images go to Docker Hub, the training code to a bucket.
- Finally we let Vertex AI Pipelines run all four steps in order, on Google's machines, with one command.
Complete Tutorial 0 - Setup & Installs first. It covers a GCP account and project, VS Code, Git, Docker Desktop and uv. This tutorial also builds on the model training and model deployment tutorials.
You will create your own service account and buckets in your own GCP project. Training jobs and endpoints run on machines billed to that project, so do the Clean Up step at the end.
git clone https://github.com/dlops-io/ml-workflow.git
cd ml-workflowEach component lives in its own folder under src/, with its own container:
ml-workflow/src/
โโโ data-collector/ # Step 1
โโโ data-processor/ # Step 2
โโโ model-training/ # Step 3 (training job + trainer package)
โโโ workflow/ # Runs the pipeline; also defines the Model Deploy step (model.py)
By completing this tutorial, you'll gain experience with:
- Breaking an ML system into components, each in its own container.
- Pipelines as code: describing steps and their order in Python with
kfp, and compiling them to a pipeline definition (.yaml). - Running pipelines on Vertex AI: serverless execution, where Google provides the machines for each step.
- Monitoring runs in the console: which step ran, how long it took, and where it failed.
- Container images for pipelines: why each step's image has to be published to a registry that Vertex AI can pull from.
- A component is one step: a container image plus the command to run in it (for example the data collector image running
cli.py --search ...). - A pipeline is a set of components and the order they run in (
.after(...)inworkflow/cli.py). - A run (a
PipelineJob) is one execution of a pipeline. Each run shows up in the console with its own graph, logs and status.
As in the mega-pipeline tutorial, the steps do not call each other. Each step reads its input from the GCS bucket and writes its output back there (raw.zip โ clean.zip โ model/). The pipeline only decides when each step starts.
On your laptop, docker-shell.sh builds an image locally. Vertex AI Pipelines runs on Google's machines, which cannot see your laptop, so the data collector and processor images are published to Docker Hub (dlops/cheese-app-data-collector, dlops/cheese-app-data-processor) and the pipeline pulls them from there. We have already published them for this tutorial; see Build & Push Images in the Appendix if you want to publish your own.
- Container components (data collector, data processor) run one of our images.
The same image works two ways, and
docker-entrypoint.shpicks one: started bydocker-shell.shwith no arguments, it opens a shell for you; started by the pipeline with arguments (for examplecli.py --search --nums 10 ..., set insrc/workflow/cli.py), it runs that command and exits. - Python components (model training, model deploy in
workflow/model.py) are small Python functions thatkfpruns in a standard Python image; they call the Vertex AI SDK to start a training job or deploy a model.
Search for each of these in the GCP search bar and click Enable:
- Agent Platform API (formerly "Vertex AI API"; service name
aiplatform.googleapis.com) - Cloud Storage API (usually already enabled)
Or from a terminal with gcloud: gcloud services enable aiplatform.googleapis.com storage.googleapis.com
We do not want any secrets in Git, so we keep them outside the repository. At the same level as the ml-workflow folder, create two folders:
|-ml-workflow
|-persistent-folder # scratch space the containers can keep between runs
|-secrets
| Name | ml-workflow |
| Key file | secrets/ml-workflow.json |
Roles
| Role | ID | Why |
|---|---|---|
| Storage Admin | roles/storage.admin |
Read and write the bucket |
| Vertex AI Administrator | roles/aiplatform.admin |
Upload models, create endpoints, run jobs |
| Service Account User | roles/iam.serviceAccountUser |
Lets pipeline steps run as this account |
Steps
- Open IAM & Admin > Service accounts (direct link) and click Create service account.
- Enter the name
ml-workflowand click Create and continue. - Add each role from the table above, then click Done.
- Click the new account's โฎ (Actions) > Manage keys > Add key > Create new key > JSON > Create. A key file downloads.
- Move the file into your secrets folder and rename it to
ml-workflow.json.
The console may list "Vertex AI Administrator" under the new Agent Platform name; search for
aiplatform.adminif you cannot find it.If Create new key is greyed out or fails with a policy error, your organization blocks key creation (the default for new Google Cloud organizations). Use a personal project, or ask your organization admin to allow it.
- Go to
https://console.cloud.google.com/storage/browserand create, in regionus-central1:- a data/model bucket, for example
cheese-app-ml-workflow-<your-name>: holdsraw.zip,clean.zip, the trainedmodel/and the pipeline's working files (pipeline_root/). - a trainer-code bucket, for example
cheese-app-trainer-code-<your-name>: holds the packaged training code and the training job outputs.
- a data/model bucket, for example
- Bucket names are global, so make them unique.
Every docker-shell.sh under src/ sets the same few variables at the top. Edit them in all four files:
export GCP_PROJECT="<your-project-id>"
export GCS_BUCKET_NAME="<your-data-bucket>"
export GCS_PACKAGE_URI="gs://<your-trainer-code-bucket>" # model-training, workflow
export GCS_SERVICE_ACCOUNT="ml-workflow@<your-project-id>.iam.gserviceaccount.com" # workflowEach component has its own container. For each one, cd into its folder and run sh docker-shell.sh; this builds the image and drops you into a shell inside it.
cd src/data-collector
sh docker-shell.sh
python cli.py --search --nums 10 --query "brie cheese" "gouda cheese" "gruyere cheese" "parmigiano cheese"This downloads up to 10 images per label into folders named after each query:
raw/
โโโ brie cheese/
โโโ gouda cheese/
โโโ gruyere cheese/
โโโ parmigiano cheese/
and uploads them as raw.zip to your data bucket. Check the bucket to confirm.
cd src/data-processor
sh docker-shell.sh
python cli.py --cleanThis downloads raw.zip, removes duplicates and invalid images, and uploads clean.zip to your bucket. (python cli.py --prepare additionally converts the clean images to TFRecords and uploads tfrecords.zip.)
cd src/model-training
sh docker-shell.sh
sh package-trainer.sh
python cli.py --trainpackage-trainer.shpackages the training code inpackage/ascheese-app-trainer.tar.gzand uploads it to your trainer-code bucket.python cli.py --trainstarts a custom training job on Vertex AI that runs that package. Follow it under Agent Platform > Training in the console.- When it finishes, the trained model (with its serving function) is saved to
gs://<your-data-bucket>/model.
To keep training reproducible, the trainer downloads a fixed, already-cleaned dataset from GitHub (
dlops-io/datasets,cheese_4_labels.zip) rather than theclean.zipproduced in step 2.
The pipeline runs on Google's machines, which cannot see anything on your laptop. Before it can run a step, that step's code must be somewhere Google can fetch it:
| Pipeline step | What gets published | Where to | How |
|---|---|---|---|
| Data Collector | the container image | Docker Hub (dlops/cheese-app-data-collector) |
sh docker-build-push.sh in src/data-collector |
| Data Processor | the container image | Docker Hub (dlops/cheese-app-data-processor) |
sh docker-build-push.sh in src/data-processor |
| Model Training | the training code (cheese-app-trainer.tar.gz) |
your trainer-code bucket | sh package-trainer.sh, already done in Part 1 |
| Model Deploy | nothing | n/a | it is a small Python function inside src/workflow |
- You can skip the Docker Hub push. The
dlopsimages are already published, and the pipeline uses them by default. Push your own only if you change the data-collector or data-processor code; see Build & Push Images in the Appendix. - The pipeline only pulls, never builds. If you change a component's code and do not push again, the pipeline keeps running the old version.
- The same image runs in both places. Started by
docker-shell.shwith no arguments, it opens a shell. Started by the pipeline with arguments (e.g.cli.py --search ...), it runs that command and exits.docker-entrypoint.shdecides which.
All pipeline commands run from the workflow container:
cd src/workflow
sh docker-shell.shpython cli.py --data_collectorThis wraps the data collector image as a pipeline component, compiles it to data_collector.yaml and submits a PipelineJob. Open data_collector.yaml to see what was generated, then watch the run under Agent Platform > Pipelines (direct link).
python cli.py --pipelineThis chains all four steps (Data Collector โ Data Processor โ Model Training โ Model Deploy), compiles them to pipeline.yaml and submits one run. Each step starts only after the previous one succeeds. In the console you should see a graph like this:
A full run takes a while: training alone is several minutes, and deploying an endpoint often takes 10โ20 minutes.
Useful when a step fails and you want to rerun just that one:
python cli.py --data_collector
python cli.py --data_processor
python cli.py --model_training
python cli.py --model_deploypython cli.py --sampleA small pipeline of Python components (square, add, square root) that shows how values pass from one step to the next, with no containers of our own.
The Model Deploy step creates an endpoint on an n1-standard-4 machine that bills your project every hour, even when nobody calls it. When you are done:
In the console
- Go to Agent Platform > Models > Endpoints, or open https://console.cloud.google.com/vertex-ai/online-prediction/endpoints.
- Click your endpoint's name. On the row for your model, click Actions (โฎ) > Undeploy model from endpoint, then Undeploy.
- Back on the Endpoints list, select the endpoint, click Actions (โฎ) > Delete endpoint.
Or from the workflow container
gcloud ai endpoints list --region=us-central1
gcloud ai endpoints describe <ENDPOINT_ID> --region=us-central1 # deployedModels: id
gcloud ai endpoints undeploy-model <ENDPOINT_ID> --region=us-central1 --deployed-model-id=<DEPLOYED_MODEL_ID>
gcloud ai endpoints delete <ENDPOINT_ID> --region=us-central1Pipeline runs, training jobs and registered models do not keep running, so they do not need cleaning up. Your model in the registry is at https://console.cloud.google.com/vertex-ai/models.
| Where | Command | What it does |
|---|---|---|
src/data-collector |
python cli.py --search --nums 10 --query "brie cheese" ... |
Download images, upload raw.zip |
src/data-processor |
python cli.py --clean |
Clean images, upload clean.zip |
src/model-training |
sh package-trainer.sh then python cli.py --train |
Package trainer code, start a training job |
src/data-collector, src/data-processor |
sh docker-build-push.sh (optional) |
Publish your own image to Docker Hub |
src/workflow |
python cli.py --pipeline |
Run the whole pipeline on Vertex AI |
src/workflow |
python cli.py --data_collector (or --data_processor, --model_training, --model_deploy) |
Run one step as a pipeline |
src/workflow |
python cli.py --sample |
Toy pipeline |
Then undeploy and delete the endpoint when you are done.
This has already been done for this tutorial: the pipeline uses dlops/cheese-app-data-collector and dlops/cheese-app-data-processor from Docker Hub. To publish your own versions:
- Sign up on Docker Hub and create an Access Token.
- Log in:
docker login -u <USER NAME>and paste the token when asked for a password. - In
src/data-collector(andsrc/data-processor), setDOCKER_USERNAMEindocker-build-push.shand runsh docker-build-push.sh. It builds the image forlinux/amd64(what Vertex AI runs) and pushes it to Docker Hub. On Apple-silicon Macs the build is emulated, so it is slower thandocker-shell.sh. - Update
DATA_COLLECTOR_IMAGEandDATA_PROCESSOR_IMAGEat the top ofsrc/workflow/cli.pyto your image names.
The pipeline never builds or pushes images; it only pulls them from Docker Hub. If you change the code in
src/data-collectororsrc/data-processor, the pipeline keeps running the old version until you runsh docker-build-push.shagain.
- Run Docker commands from Git BASH.
- Always quote
$(pwd): Windows paths often contain spaces. - If you see
the input device is not a TTY, prefix thedocker runcommand indocker-shell.shwithwinpty.



