FastAPI and Cloud Function deployment for ML model serving with lifecycle awareness and reproducibility.
This project demonstrates model serving patterns for MLOps:
- FastAPI Web Service: Containerized model serving with schema validation
- Cloud Function: Serverless inference endpoint
- Comparative Analysis: Architecture trade-offs and lifecycle implications
ML Lifecycle Position:
Data → Training → [Model Artifact] → API Serving → Consumer Applications
↓
model.pkl
↓
[This Deployment Layer]
- Local Setup
- API Usage
- Model Details
- Live Deployments
- Deployment Instructions
- Lifecycle & Architecture
- Comparative Analysis
- Python 3.10 or higher
- pip
- Git
- Clone the repository:
git clone https://github.com/ParthPatel0226/mlops-milestone1-model-serving.git
cd mlops-milestone1-model-serving- Create virtual environment:
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Train model (if needed):
python train_model.pyThis creates model.pkl using scikit-learn's Iris dataset.
- Run FastAPI server:
uvicorn main:app --host 0.0.0.0 --port 8000 --reload- Test the API:
# Health check
curl http://localhost:8000/
# Prediction
curl -X POST "http://localhost:8000/predict" \
-H "Content-Type: application/json" \
-d '{"features": [5.1, 3.5, 1.4, 0.2]}'Returns service status and model loading confirmation.
Response:
{
"status": "healthy",
"service": "ML Model Serving API",
"model_loaded": true
}Accepts 4 features and returns predicted Iris species.
Request Schema (Pydantic):
{
"features": [5.1, 3.5, 1.4, 0.2]
}Response Schema (Pydantic):
{
"prediction": 0,
"prediction_label": "setosa",
"confidence": 0.98,
"model_version": "1.0.0"
}Example using Python requests:
import requests
response = requests.post(
"https://ml-api-202083721674.us-central1.run.app/predict",
json={"features": [5.1, 3.5, 1.4, 0.2]}
)
print(response.json())Returns metadata about the loaded model.
Response:
{
"model_type": "LogisticRegression",
"model_version": "1.0.0",
"input_features": 4,
"output_classes": 3,
"class_names": ["setosa", "versicolor", "virginica"]
}- Dataset: Iris classification (scikit-learn built-in)
- Algorithm: Logistic Regression
- Features: 4 (sepal length, sepal width, petal length, petal width)
- Classes: 3 (setosa, versicolor, virginica)
- Accuracy: ~100% on test set
Deterministic Loading: The model is loaded once at application startup (not per-request) to ensure:
- ✅ Fast inference (no repeated disk I/O)
- ✅ Consistent predictions
- ✅ Reproducible behavior
# Loaded at startup (main.py)
model = joblib.load('model.pkl')
@app.post("/predict")
def predict(request: PredictRequest):
# Model already in memory - fast prediction
prediction = model.predict(features)Reproducibility Test:
python train_model.py
# Verifies that model.predict() returns identical results across loadsService URL: https://ml-api-202083721674.us-central1.run.app
Endpoints:
- Health:
GET / - Prediction:
POST /predict - Model Info:
GET /model-info
Artifact Registry Image:
us-central1-docker.pkg.dev/project-c6c5efaf-f038-49a2-9fa/ml-models/ml-api:v1
Test Command:
curl -X POST "https://ml-api-202083721674.us-central1.run.app/predict" \
-H "Content-Type: application/json" \
-d '{"features": [5.1, 3.5, 1.4, 0.2]}'Response:
{
"prediction": 0,
"prediction_label": "setosa",
"confidence": 0.98,
"model_version": "1.0.0"
}Function URL: https://us-central1-project-c6c5efaf-f038-49a2-9fa.cloudfunctions.net/predict
Test Command:
curl -X POST "https://us-central1-project-c6c5efaf-f038-49a2-9fa.cloudfunctions.net/predict" \
-H "Content-Type: application/json" \
-d '{"features": [5.1, 3.5, 1.4, 0.2]}'Response:
{
"prediction": 0,
"prediction_label": "setosa",
"confidence": 0.9766,
"model_version": "1.0.0",
"deployment_type": "cloud_function"
}Both deployments tested and working successfully!
- Google Cloud SDK installed
- GCP project with billing enabled
- Docker installed
- Enable required APIs:
gcloud services enable run.googleapis.com
gcloud services enable artifactregistry.googleapis.com
gcloud services enable cloudbuild.googleapis.com- Create Artifact Registry repository:
gcloud artifacts repositories create ml-models \
--repository-format=docker \
--location=us-central1 \
--description="ML model serving images"- Configure Docker authentication:
gcloud auth configure-docker us-central1-docker.pkg.dev- Build Docker image:
docker build -t us-central1-docker.pkg.dev/PROJECT_ID/ml-models/ml-api:v1 .- Push to Artifact Registry:
docker push us-central1-docker.pkg.dev/PROJECT_ID/ml-models/ml-api:v1- Deploy to Cloud Run:
gcloud run deploy ml-api \
--image us-central1-docker.pkg.dev/PROJECT_ID/ml-models/ml-api:v1 \
--region us-central1 \
--allow-unauthenticated \
--platform managedLocation: cloud_function/ directory
main.py- Function entry point with prediction logicrequirements.txt- Function dependenciesmodel.pkl- Trained model artifact
cd cloud_function
# Grant required permissions (one-time)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role=roles/cloudbuild.builds.builder
# Deploy function
gcloud functions deploy predict \
--gen2 \
--runtime python311 \
--region us-central1 \
--source . \
--entry-point predict \
--trigger-http \
--allow-unauthenticated┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Data │────▶│ Training │────▶│ Model │────▶│ Deployment │
│ Collection │ │ & Tuning │ │ Artifact │ │ & Serving │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
↓ ↓
model.pkl ┌────────────────┐
│ [THIS PROJECT] │
│ FastAPI/CF │
└────────────────┘
↓
┌────────────────┐
│ Consumers │
│ (Apps/Users) │
└────────────────┘
This deployment sits at the "Serving" stage, bridging model artifacts and consumer applications.
Startup Phase:
# Application initialization (happens once)
model = joblib.load('model.pkl') # Deterministic artifact loading
app = FastAPI() # Initialize API frameworkRequest Phase:
# Per-request processing (happens many times)
1. Client sends POST /predict with features
2. Pydantic validates input schema
3. Model.predict(features) - uses pre-loaded model
4. Pydantic formats output schema
5. Return JSON response to clientKey Design Decisions:
- ✅ Load model at startup (not per-request) → Low latency
- ✅ Pydantic schemas → Input validation & output consistency
- ✅ Stateful container → Model stays in memory between requests
Where to add observability:
@app.post("/predict")
def predict(request: PredictRequest):
# 1. Log request count (Prometheus counter)
# 2. Start latency timer (Prometheus histogram)
prediction = model.predict(features)
# 3. Log prediction distribution (for drift detection)
# 4. Track inference errors (alert on spike)
# 5. Monitor memory usage (model size)
return response| Aspect | FastAPI (Cloud Run) | Cloud Function |
|---|---|---|
| Deployment Pattern | Containerized web service | Serverless function |
| State Management | Stateful - model stays in memory | Stateless - reloads each cold start |
| Artifact Loading | Once at startup | Per cold start (15+ min idle) |
| Cold Start Latency | ~2-4 seconds (container start) | ~1-2 seconds (lighter weight) |
| Warm Latency | <100ms (model in memory) | <100ms (if warm) |
| Concurrency | Multiple requests share instance | One request per instance |
| Cost Model | Pay for CPU/memory-time | Pay per invocation + compute |
| Best For | High throughput, low latency | Sporadic/event-driven workloads |
FastAPI Container (Stateful):
- ✅ Model loaded once → Fast subsequent requests
- ✅ Can cache intermediate results
- ✅ Better for consistent traffic patterns
- ❌ Heavier cold starts (full container)
- ❌ More complex deployment (Docker)
Cloud Function (Stateless):
- ✅ Lighter cold starts (no container overhead)
- ✅ Simpler deployment (just code)
- ✅ Auto-scales to zero (cost-efficient for low traffic)
- ❌ Model reloads after idle periods
- ❌ No shared state between invocations
FastAPI Approach:
# Global scope - loads once at container start
model = joblib.load('model.pkl')
@app.post("/predict")
def predict(...):
return model.predict(...) # Fast - model already in memoryCloud Function Approach:
# Global scope - loads at cold start
model = None
def predict(request):
global model
if model is None: # Cold start
model = joblib.load('model.pkl') # Reload penalty
return model.predict(...) # Fast once loadedContainer (Higher Reproducibility):
- ✅ Pinned Python version in Dockerfile
- ✅ Exact dependency versions
- ✅ Same OS environment (Alpine/Debian)
- ✅ Immutable image in registry
Function (Lower Reproducibility):
⚠️ Runtime managed by GCP (updates automatically)⚠️ Less control over system dependencies⚠️ Potential version drift over time
Choose FastAPI Container when:
- Steady traffic with predictable patterns
- Latency-sensitive applications (every ms counts)
- Need persistent connections or state
- Complex dependencies or custom runtime
Choose Cloud Function when:
- Sporadic/event-driven workloads
- Cost optimization for low traffic
- Simple inference logic
- Rapid prototyping/experimentation
mlops-milestone1-model-serving/
├── main.py # FastAPI application
├── train_model.py # Model training script
├── model.pkl # Trained model artifact
├── requirements.txt # Python dependencies (pinned versions)
├── Dockerfile # Container definition for Cloud Run
├── cloud_function/ # Cloud Function code
│ ├── main.py # Function entry point
│ ├── requirements.txt # Function dependencies
│ └── model.pkl # Model artifact (copy)
├── screenshots/ # Deployment evidence
└── README.md # This file
- Python 3.11 - Programming language
- FastAPI 0.104.1 - Web framework
- Pydantic 2.12.5 - Schema validation
- scikit-learn 1.8.0 - ML model training
- Uvicorn 0.24.0 - ASGI server
- Docker - Containerization
- GCP Cloud Run - Container deployment
- GCP Cloud Functions - Serverless deployment
- GCP Artifact Registry - Container image storage
Parth Patel
MS in Management Information Systems
University of Illinois Chicago
GitHub: https://github.com/ParthPatel0226
MIT License - see LICENSE file for details