Deploying ML Models on AWS, Azure and GCP

AWS, Azure and Google Cloud all host models behind managed endpoints, but each expects a different serving contract. This guide runs one real model behind all three contracts locally, including Microsoft's own inference server, and shows what changes between clouds and what should stay exactly the same.

GloryTecks Team

Training & Placement Faculty

6 May 2026 8 min read

Last updated: 8 October 2026

Share:

Every major cloud will host a trained model behind a managed endpoint: Amazon SageMaker AI on AWS, Azure Machine Learning on Azure, and Gemini Enterprise Agent Platform on Google Cloud, which now holds what was Vertex AI: Vertex AI documentation links redirect to it. AWS renamed its service on 3 December 2024; Amazon SageMaker AI is what used to be Amazon SageMaker, and its API names did not change (AWS: What is SageMaker AI).

The steps to deploy ML models look alike on all three: package the model, wrap it in code the platform knows how to call, create an endpoint and send traffic. The part that really differs is that wrapper, the serving contract. This guide runs one model behind all three contracts on a laptop and compares them.

The managed options

AWSAzureGoogle Cloud
ServiceAmazon SageMaker AIAzure Machine LearningGemini Enterprise Agent Platform
Real-time servingReal-time and serverless endpointsManaged online endpointsOnline inference endpoints
Your own codeCustom containerScoring script or custom containerCustom container
Gradual rolloutProduction and shadow variantsSeveral deployments behind one endpointTraffic split across models on one endpoint

Azure recommends managed online endpoints for real-time serving, and keeps endpoints separate from deployments so you can roll out safely, compare champion and challenger, and mirror traffic (Microsoft Learn: online endpoints). Google lets a new model join an endpoint with a small share of traffic and grow to 100% (Google Cloud: deploy to an endpoint).

Three contracts

  • SageMaker AI: a web server on port 8080 that answers GET /ping with 200 and POST /invocations with predictions. Health checks must pass within 8 minutes of startup, each request must finish within 60 seconds, and the model files appear read-only in /opt/ml/model (AWS: inference containers).
  • Google Cloud: the platform passes the port, health route, prediction route and model location in environment variables (AIP_HTTP_PORT, AIP_HEALTH_ROUTE, AIP_PREDICT_ROUTE, AIP_STORAGE_URI). Requests carry an instances array and responses a predictions array, each up to 1.5 MB (Google Cloud: custom containers).
  • Azure Machine Learning: a scoring script with an init function, called once, and a run function, called per request, with the model folder in AZUREML_MODEL_DIR (Microsoft Learn: online endpoints). Microsoft publishes the same inference server as a Python package, azmlinfsrv, for running the script locally (Microsoft Learn: inference server).

One model, run behind all three

The model predicts concrete compressive strength from a mix's ingredients and age, trained on the UCI dataset of 1,030 mixes, under CC BY 4.0 (UCI: Concrete Compressive Strength). One FastAPI app meets both container contracts:

python
import os

import joblib
import pandas as pd
from fastapi import FastAPI, HTTPException, Request, Response

# SageMaker copies the model to /opt/ml/model; Google passes its location in AIP_STORAGE_URI. MODEL_DIR overrides both locally.
MODEL_DIR = os.environ.get("MODEL_DIR", "/opt/ml/model")
model = joblib.load(os.path.join(MODEL_DIR, "model.joblib"))
FEATURES = list(model.feature_names_in_)
app = FastAPI()


def predict(body):
    if not isinstance(body, dict) or not isinstance(body.get("instances"), list):   # a client error, not a server fault
        raise HTTPException(status_code=400, detail='expected a JSON object with an "instances" list')
    return [round(float(v), 2) for v in model.predict(pd.DataFrame(body["instances"], columns=FEATURES))]


# Amazon SageMaker AI contract: port 8080, GET /ping, POST /invocations
@app.get("/ping")
def ping():
    return Response(status_code=200)


@app.post("/invocations")
async def invocations(request: Request):
    body = await request.json()
    return {"predictions": predict(body)}


# Google Cloud (Gemini Enterprise Agent Platform) contract: the platform sets the routes and the port
@app.get(os.environ.get("AIP_HEALTH_ROUTE", "/health"))
def health():
    return {"status": "ok"}


@app.post(os.environ.get("AIP_PREDICT_ROUTE", "/predict"))
async def vertex_predict(request: Request):
    body = await request.json()
    return {"predictions": predict(body)}

The Azure scoring script:

python
import json
import os

import joblib
import pandas as pd

model = None


def init():                                    # Azure Machine Learning calls this once when the deployment starts
    global model
    model_dir = os.environ.get("AZUREML_MODEL_DIR", "model")
    model = joblib.load(os.path.join(model_dir, "model.joblib"))


def run(raw_data):                             # and this for every request to /score
    rows = json.loads(raw_data)["data"]
    frame = pd.DataFrame(rows, columns=list(model.feature_names_in_))
    return {"predictions": [round(float(v), 2) for v in model.predict(frame)]}

A script starts both servers, sends the same three mixes in each platform's format, then sends requests in the wrong shape:

python
import os
import shutil
import subprocess
import sys
import time

import joblib
import pandas as pd
import requests
from sklearn.ensemble import HistGradientBoostingRegressor

mix = pd.read_csv("concrete.csv")                               # UCI Concrete Compressive Strength (CC BY 4.0)
features = [c for c in mix.columns if c != "strength_mpa"]
os.makedirs("model", exist_ok=True)
joblib.dump(HistGradientBoostingRegressor(random_state=0).fit(mix[features], mix.strength_mpa), "model/model.joblib")
rows = mix[features].head(3).values.tolist()

servers = [
    subprocess.Popen([sys.executable, "-m", "uvicorn", "app:app", "--port", "8080", "--log-level", "warning"],
                     env=dict(os.environ, MODEL_DIR="model", AIP_HEALTH_ROUTE="/health", AIP_PREDICT_ROUTE="/predict")),
    subprocess.Popen([shutil.which("azmlinfsrv"), "--entry_script", "score.py", "--port", "5005"],   # Azure's own server
                     env=dict(os.environ, AZUREML_MODEL_DIR="model"), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL),
]


def wait_for(url):
    for _ in range(120):
        try:
            if requests.get(url, timeout=1).ok:
                return
        except requests.ConnectionError:
            pass
        time.sleep(0.5)
    raise RuntimeError(f"{url} did not start")


try:
    for url in ("http://127.0.0.1:8080/ping", "http://127.0.0.1:8080/health", "http://127.0.0.1:5005/"):
        wait_for(url)
    calls = {"SageMaker AI  POST /invocations": ("http://127.0.0.1:8080/invocations", {"instances": rows}),
             "Google Cloud  POST /predict": ("http://127.0.0.1:8080/predict", {"instances": rows}),
             "Azure ML      POST /score": ("http://127.0.0.1:5005/score", {"data": rows})}
    answers = {}
    for name, (url, body) in calls.items():
        reply = requests.post(url, json=body, timeout=30)
        answers[name] = reply.json()["predictions"]
        print(f"{name:<34} {reply.status_code}  {answers[name]}")
    print(f"actual strengths (MPa): {mix.strength_mpa.head(3).round(2).tolist()}")
    print(f"same predictions from all three contracts: {len({tuple(a) for a in answers.values()}) == 1}")
    print("\nrequests in the wrong shape:")
    for name, url, body in (("SageMaker AI, 'data' instead of 'instances'", "http://127.0.0.1:8080/invocations", {"data": rows}),
                            ("Azure ML, 'instances' instead of 'data'", "http://127.0.0.1:5005/score", {"instances": rows})):
        print(f"  {name:<46} HTTP {requests.post(url, json=body, timeout=30).status_code}")
finally:
    for server in servers:
        if os.name == "nt":
            subprocess.run(["taskkill", "/T", "/F", "/PID", str(server.pid)], capture_output=True)
        else:
            server.terminate()
text
SageMaker AI  POST /invocations    200  [71.13, 68.29, 41.1]
Google Cloud  POST /predict        200  [71.13, 68.29, 41.1]
Azure ML      POST /score          200  [71.13, 68.29, 41.1]
actual strengths (MPa): [79.99, 61.89, 40.27]
same predictions from all three contracts: True

requests in the wrong shape:
  SageMaker AI, 'data' instead of 'instances'    HTTP 400
  Azure ML, 'instances' instead of 'data'        HTTP 500

What the output shows

  • The model never changed. All three contracts returned the same predictions, 71.13, 68.29 and 41.10 MPa, for mixes measured at 79.99, 61.89 and 40.27. Everything cloud-specific lived in a thin wrapper.
  • One image can serve two clouds. Google lets the platform choose the routes, so the container built for SageMaker's fixed routes also met Google's contract.
  • Bad requests behaved differently. The container checked its input and answered 400, a client error. Azure's server turned the scoring script's exception into a 500, which looks like a server fault to callers and to alerting, so a scoring script should check its own input.
  • Most deployment failures can be caught on a laptop. A wrong route, body shape or model path fails locally exactly as it would in the cloud, minutes earlier and for free.

For writing the service itself, see serving ML models with FastAPI and Docker. For rolling a new version out safely, see blue-green deployments and shadow deployment and canary releases. For the platforms beyond serving, see Vertex AI and SageMaker compared, and for keeping the bill down, see cost optimisation for ML workloads.

Choosing where to deploy

  • Follow your data and your team. The cloud that already holds your data and identity setup usually wins on effort and egress cost.
  • Match the serving mode to traffic. Always-on endpoints suit steady traffic; serverless or batch suit spiky or offline work.
  • Keep the model portable. A standard format and a thin wrapper make moving clouds a matter of rewriting the wrapper.
  • Use the platform's rollout features rather than switching all traffic at once.

Common mistakes

  • Testing for the first time in the cloud.
  • Baking the model file into the image instead of reading it from the platform's model location.
  • Returning 500 for malformed requests.
  • Cloud-specific code throughout the model, instead of in one wrapper.
  • Leaving idle endpoints running.

How do I deploy a machine learning model to the cloud?

Save the trained model, wrap it in a server or scoring script that follows the platform's serving contract, register it, create an endpoint with compute, and route traffic to it, ideally gradually. Test the wrapper locally first.

What is the difference between SageMaker, Azure ML and Vertex AI deployment?

Mostly the serving contract and the rollout tools. SageMaker AI expects /ping and /invocations on port 8080; Google's Gemini Enterprise Agent Platform, successor to Vertex AI, passes routes and port in AIP_ variables and uses instances and predictions; Azure Machine Learning runs a scoring script with init and run functions.

Can the same container run on more than one cloud?

Often, yes. In the example above, one FastAPI container met both the SageMaker AI and Google Cloud contracts, because Google lets the platform set the routes. Azure can also run custom containers.

How do I test a cloud model deployment locally?

Run your container or scoring script with the same routes, ports, environment variables and request shapes the platform uses. Azure publishes its inference server as a Python package for exactly this.

Model deployment on AWS, Azure and Google Cloud is covered in the Cloud MLOps Platforms module of the MLOps course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master MLOps?

Join GloryTecks’ expert-led MLOps training in Hyderabad with real-time projects and 100% placement support.

Explore course