MLOps · Azure
Raw data to live inference — an end-to-end MLOps pipeline
Two Azure DevOps pipelines that take an empty subscription all the way to a working app — provisioning, data refinement, training, deployment, and a natural-language layer on top.
Technologies
Azure ML · Azure DevOps · Terraform · Azure OpenAI · Container Apps · Flask · Python · Kaggle

Overview
This proof of concept runs in two phases, and between them they cover the entire distance from an empty Azure subscription to a person typing a question in plain English and getting a useful answer back.
The first phase provisions an Azure Machine Learning workspace with Terraform, pulls a raw dataset straight from Kaggle, refines it to gold tier and registers it as a versioned data asset, spins up a dedicated compute cluster, then submits a pipeline job that trains, evaluates, and registers the model — finishing by deploying that registered version to a live scoring endpoint.
The second phase adds the layer that makes it usable. Terraform deploys a GPT model through Azure OpenAI Service, and a Flask app ships to Azure Container Apps wired to both models. Because the scoring endpoint only speaks JSON, GPT sits on either side of it as a translator: plain English in, JSON to the model, JSON back, plain English out.
The Challenge
Context
An AI team wanted to see the whole production path for machine learning laid out end to end before committing to a platform. Data scientists were training models on laptops and handing over artifacts by hand — nothing versioned, nothing reproducible, and no route from a trained model to something an application could actually call. The open question wasn't whether a model could be trained; it was whether the surrounding machinery — provisioning, data preparation, training, registration, deployment, and consumption — could be made to run start to finish without a human in the loop.
Problem Space
Build a working proof of concept that walks the entire journey — an empty Azure subscription in, a natural-language answer out — with every stage automated, versioned, and repeatable by anyone on the team. And close the last mile that most ML demos skip: the deployed model speaks JSON and nothing else, so the proof had to include making it usable by a person who does not.
Approach
Phase one begins with an empty subscription and a Terraform plan. The first Azure DevOps pipeline does not assume anything exists. It runs Terraform against a bare subscription and stands up the Azure Machine Learning workspace along with every dependency it quietly relies on — the storage account that will hold datasets and job outputs, the key vault that holds secrets, the container registry for training and inference images, and the monitoring that makes runs observable after the fact. Doing this as code rather than through the portal is the decision the rest of the proof of concept rests on: because no resource is ever created by hand, the environment can be torn down completely and rebuilt identically from source control, changes to it arrive as reviewable diffs rather than as someone's undocumented click, and a second environment for staging or for another team is a matter of running the same pipeline against a different target. It also removes the most common failure mode in machine-learning work, where a model that trained fine last quarter cannot be reproduced because nobody remembers how the workspace around it was configured.
Raw Kaggle data is refined to gold tier and registered as a versioned asset. With the workspace live, the same pipeline pulls the source dataset directly from Kaggle. Making ingestion a pipeline step rather than a manual download matters more than it sounds: it means the run is genuinely self-contained, with no file sitting on an engineer's laptop that the rest of the process silently depends on. From there the extract is promoted through a medallion progression — cleaned of malformed and duplicate records, typed correctly, and put through feature engineering — until it reaches a gold-tier state that is ready to train against without further handling. That gold dataset is then registered as a named, versioned data asset inside the Azure ML workspace, and every training run afterwards reads from that named version rather than from a path. The effect is that two training runs a month apart are provably reading identical data, and when a model's behaviour changes it is possible to say with certainty whether the data moved, the code moved, or neither did.
Training runs on compute that exists only while it is needed. The pipeline provisions a dedicated Azure ML compute cluster and submits the training work to it as a pipeline job. That job handles the full modelling sequence in one submission: it trains against the registered gold dataset, evaluates the result against held-out data, and registers the winning model into the workspace registry with its metrics and lineage attached. Two things follow from structuring it this way. First, promotion becomes an evidence-based decision rather than a social one — a model advances because its evaluation output says it should, not because it happened to be the last thing someone ran in a notebook, and every registered version carries a pointer back to the exact dataset version and code that produced it. Second, the cluster scales up when a job is submitted and back down when the queue empties, so training capacity is billed only while training is actually happening and the proof of concept carries no idle compute cost between runs.
The registered model is deployed to a live endpoint in the same run. The freshly registered model version is deployed to a managed online endpoint inside the workspace, which exposes it as an authenticated REST API that anything on the network can call. This is the point where a great deal of machine-learning work quietly stalls — a model sits in a registry, well-trained and well-documented, with no route to an application and a deployment ticket somewhere in a backlog. Here it is simply the last step of the same automated sequence that started with an empty subscription, so the distance between 'trained' and 'callable' is zero human actions. The previous model version remains registered and deployable throughout, which means rolling back is a redeploy of a known-good version rather than a retraining exercise under pressure.
Phase two adds a language layer that makes the model usable by people. A second Azure DevOps pipeline picks up where the first one finished, and again Terraform does the provisioning: it deploys a GPT model through Azure OpenAI Service, so the language capability arrives as code in exactly the same reviewable, repeatable form as the rest of the platform. A containerized Flask application then ships to Azure Container Apps as the user-facing surface. It needs to reach both models, so the phase-one scoring endpoint, the Azure OpenAI endpoint, and their credentials are injected as configuration at deploy time rather than baked into the image — the same container image therefore runs unchanged against whichever environment it is pointed at, and no endpoint URI or key ever lives in the application source.
GPT sits on both sides of the inference call as a translator. This is the part that turns a working pipeline into something a person can actually use, and it is the step most demonstrations skip. The scoring endpoint speaks JSON and only JSON: it expects a payload matching a precise schema and it returns a prediction in the same shape. That is exactly right for a machine and completely useless to a non-technical user, who has a question, not a schema. So the GPT model is placed on both ends of the call. A user types an instruction in ordinary language; GPT interprets it and constructs the exact JSON payload the predictive model expects; the model scores it and returns JSON; GPT reads that result back and answers the user in plain language. Two translations wrapped around a single inference call, with the schema never surfacing. The wider point is that the predictive model was never made more approachable — it stayed strict and typed, which is what you want from something making predictions — and the accessibility was added as a layer around it rather than as a compromise inside it.
Outcomes
- The entire platform rebuilds from an empty Azure subscription with two pipeline runs and no manual steps
- Raw Kaggle data reaches gold tier, trains a model, and lands on a live endpoint in a single automated sequence
- Every registered model version traces back to the exact dataset version, code, and evaluation metrics that produced it
- Training compute scales down between jobs — no idle cost while nothing is training
- A non-technical user queries a JSON-only model in plain English and gets a plain-English answer back
- No endpoints, keys, or connection strings hardcoded — the app is configured at deploy time, not built around one environment
More Work
View allFacing a similar architectural challenge?
A 30-minute architecture review usually surfaces the quickest reliability and FinOps wins — no pitch, just a look at what you've got.


