Deployment Project
Build, test, containerize, release, monitor, and maintain an end-to-end machine learning prediction service.
The individual MLOps tools become meaningful when they support one complete, operable product. In this project, users submit property features and receive an estimated price. The model is simple, but the delivery system includes data contracts, evaluation, artifacts, APIs, containers, identities, deployment, observability, and maintenance.
The objective is not to claim that a tutorial model can replace a professional valuation. The objective is to demonstrate a disciplined path from versioned data to a safely operated prediction API and to document its limitations and remaining risks.
Project Outcomes
- Train and evaluate a reproducible regression model from versioned data.
- Package model, preprocessing, metadata, schema, and evaluation evidence as an immutable release.
- Expose typed prediction, liveness, readiness, and version behavior through FastAPI.
- Build a minimal non-root Docker image and test it locally.
- Run automated code, data, model, contract, container, and security checks.
- Deploy to a safe environment using a suitable managed container or model-serving platform.
- Add workload identity, secrets, limits, structured logging, metrics, dashboards, and alerts.
- Perform a canary release, demonstrate rollback and recovery, and publish operational documentation.
Architecture
Client
-> HTTPS gateway or load balancer
-> authenticated FastAPI service
-> validated preprocessing + versioned model
-> structured logs, metrics, and traces
-> dashboards and alerts
Release path
Versioned source + dataset
-> train and evaluate
-> model registry or protected artifact store
-> container build and scan
-> staging tests
-> canary production release
-> monitor, promote, or roll backSuccess Criteria
| Area | Acceptance example |
|---|---|
| Model | Candidate beats a documented baseline without critical slice regression |
| API | Valid requests return typed results and invalid requests receive safe client errors |
| Performance | Representative p95 latency and throughput meet chosen objectives |
| Reliability | Health checks, bounded concurrency, graceful shutdown, and rollback work |
| Security | No embedded secrets, non-root image, restricted identity, validation, and authenticated production access |
| Observability | Traffic, errors, latency, model version, prediction distribution, and cost are visible |
| Reproducibility | Data, source, dependency, model, image, schema, and configuration versions are traceable |
| Operations | Ownership, limitations, deployment, incident, recovery, maintenance, and retirement are documented |
Repository Structure
house-price-mlops/
├── app/
│ ├── main.py
│ ├── inference.py
│ ├── schemas.py
│ └── settings.py
├── training/
│ ├── train.py
│ ├── evaluate.py
│ └── data_contract.py
├── tests/
│ ├── test_api.py
│ ├── test_model.py
│ └── test_contract.py
├── artifacts/ # local demo; production uses governed storage
│ ├── model.joblib
│ └── metadata.json
├── deployment/
├── .github/workflows/ci.yml
├── Dockerfile
├── requirements.txt
├── README.md
├── MODEL_CARD.md
└── RUNBOOK.md1. Define the Data Contract
For the tutorial, use bedrooms, size in square feet, and a location score. Define types, units, valid ranges, missing-value behavior, target meaning, source, freshness, owner, and split strategy before training. Real valuation requires substantially richer time, geography, market, and bias analysis.
dataset: house_prices_v1
features:
bedrooms: {type: integer, minimum: 0, maximum: 20}
size_sqft: {type: number, exclusive_minimum: 0, maximum: 50000}
location_score: {type: number, minimum: 0, maximum: 10}
target:
name: sale_price_usd
type: number
split:
strategy: time_aware
owner: housing-ml-team2. Train Reproducibly
Use a reviewed training script rather than hidden notebook state. Pin dependencies, record the source and data versions, set seeds where appropriate, and store preprocessing and the estimator as one pipeline so training and serving transformations remain compatible.
import json
from pathlib import Path
import joblib
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
FEATURES = ["bedrooms", "size_sqft", "location_score"]
train = pd.read_csv("data/train.csv")
test = pd.read_csv("data/test.csv")
model = Pipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
("regressor", Ridge(alpha=1.0))
])
model.fit(train[FEATURES], train["sale_price_usd"])
mae = float(mean_absolute_error(
test["sale_price_usd"], model.predict(test[FEATURES])
))
Path("artifacts").mkdir(exist_ok=True)
joblib.dump(model, "artifacts/model.joblib")
Path("artifacts/metadata.json").write_text(json.dumps({
"model_version": "house-price-1.0.0",
"data_version": "houses-2026-07-01",
"features": FEATURES,
"test_mae": mae
}, indent=2))Only deserialize artifacts produced by a trusted pipeline because formats such as joblib and pickle can execute code. Production artifacts belong in a protected registry with checksums, provenance, evaluation evidence, approval, and restricted access.
3. Evaluate and Register
- Compare the candidate with a simple baseline and any production champion.
- Measure error distributions and relevant regional, size, and price slices—not one global metric alone.
- Test missing values, ranges, boundaries, outliers, malformed input, and training-serving compatibility.
- Record latency, memory, artifact size, dependency results, lineage, and estimated serving cost.
- Document intended use, exclusions, limitations, fairness risks, and human oversight in a model card.
- Register passing candidates, but keep production approval as a separate authorized decision.
[ ] Data contract and leakage checks pass
[ ] Candidate improves on baseline
[ ] Important slices meet accepted error thresholds
[ ] Boundary and robustness tests pass
[ ] Artifact integrity and dependency checks pass
[ ] Latency, memory, image size, and cost are acceptable
[ ] Model card, lineage, owner, and approval evidence are complete4. Define Typed API Schemas
from pydantic import BaseModel, ConfigDict, Field
class HouseFeatures(BaseModel):
model_config = ConfigDict(extra="forbid")
bedrooms: int = Field(ge=0, le=20)
size_sqft: float = Field(gt=0, le=50000)
location_score: float = Field(ge=0, le=10)
class PredictionResponse(BaseModel):
prediction_id: str
estimated_price_usd: float
model_version: str
warning: str = "Demonstration estimate; not a professional valuation."5. Build the FastAPI Service
from contextlib import asynccontextmanager
from pathlib import Path
import json
import time
import uuid
import joblib
import pandas as pd
from fastapi import FastAPI, HTTPException
from app.schemas import HouseFeatures, PredictionResponse
model = None
metadata = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global model, metadata
model = joblib.load("artifacts/model.joblib")
metadata = json.loads(Path("artifacts/metadata.json").read_text())
yield
model = None
app = FastAPI(title="House Price API", version="1.0.0", lifespan=lifespan)
@app.get("/health/live")
def live():
return {"status": "alive"}
@app.get("/health/ready")
def ready():
if model is None:
raise HTTPException(status_code=503, detail="Model is not ready")
return {"status": "ready", "model_version": metadata["model_version"]}
@app.post("/v1/predict", response_model=PredictionResponse)
def predict(features: HouseFeatures):
prediction_id = str(uuid.uuid4())
started = time.perf_counter()
frame = pd.DataFrame([features.model_dump()])
value = max(0.0, float(model.predict(frame)[0]))
# Emit a structured event with prediction ID, version, status,
# and duration; avoid logging raw feature values by default.
duration_ms = round((time.perf_counter() - started) * 1000, 2)
return PredictionResponse(
prediction_id=prediction_id,
estimated_price_usd=round(value, 2),
model_version=metadata["model_version"]
)Add structured logging, metrics, trace context, global safe exception handling, request-size limits, timeouts, bounded concurrency, and graceful shutdown. Liveness checks only the process; readiness becomes successful after the verified model is loaded and warmed.
6. Test the API and Model
from fastapi.testclient import TestClient
from app.main import app
def test_prediction_contract():
with TestClient(app) as client:
response = client.post("/v1/predict", json={
"bedrooms": 3,
"size_sqft": 1800,
"location_score": 8.0
})
assert response.status_code == 200
body = response.json()
assert body["estimated_price_usd"] >= 0
assert body["model_version"] == "house-price-1.0.0"
assert body["prediction_id"]
def test_invalid_size_is_rejected():
with TestClient(app) as client:
response = client.post("/v1/predict", json={
"bedrooms": 3, "size_sqft": -10, "location_score": 8
})
assert response.status_code == 422| Test | Coverage |
|---|---|
| Unit and model | Transformations, baseline, slices, boundaries, and reproducibility |
| Contract | Schemas, status codes, health, version, and errors |
| Integration | Artifacts, secrets, gateway, telemetry, and deployment target |
| Security | Identity, authorization, validation, limits, secrets, and image scan |
| Load | Throughput, p95/p99 latency, memory, concurrency, and cost |
| Resilience | Dependency timeout, overload, restart, node loss, and graceful shutdown |
| Release | Readiness, synthetic prediction, canary comparison, and rollback |
7. Containerize
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN groupadd --system app && useradd --system --gid app app
WORKDIR /service
COPY requirements.txt .
RUN pip install --no-cache-dir --require-hashes -r requirements.txt
COPY app ./app
COPY artifacts ./artifacts
RUN chown -R app:app /service
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]docker build -t house-price-api:1.0.0 .
docker run --rm -p 8000:8000 house-price-api:1.0.0
curl http://localhost:8000/health/ready
curl -X POST http://localhost:8000/v1/predict \
-H 'Content-Type: application/json' \
-d '{"bedrooms":3,"size_sqft":1800,"location_score":8}'Lock dependency hashes, add a .dockerignore, scan the image, generate an inventory, and record its digest and provenance. Large production models can be downloaded from a verified registry at startup instead of copied into the image.
8. Add CI/CD Gates
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- run: pip install --require-hashes -r requirements-dev.txt
- run: ruff check .
- run: pytest -q
- run: python training/evaluate.py --verify-artifacts
- run: docker build -t house-price-api:${{ github.sha }} .
# Add organization-approved secret, dependency, image,
# infrastructure, provenance, and policy checks.Protect untrusted pull requests from production secrets and cloud access. Build once, record the immutable image digest, and promote the same artifact through staging and production rather than rebuilding it differently.
9. Select a Deployment Target
| Target | Good fit | Trade-off |
|---|---|---|
| Managed model endpoint | Integrated model registry, autoscaling, and prediction operations | Platform conventions and pricing |
| Serverless container | Stateless service with compatible startup and resource needs | Cold starts and platform limits |
| Managed container service | Flexible APIs and workers without Kubernetes operations | Less low-level orchestration control |
| Kubernetes | Existing platform or complex serving genuinely requires it | Highest operational complexity |
| Virtual machine | Specialized runtime or learning environment | Team owns patching, scaling, and host resilience |
Prefer a simple managed container or model endpoint for this project. Deploy infrastructure from reviewed code, and separate development, staging, and production identities, data, configuration, and permissions.
10. Secure and Configure Runtime
- Authenticate callers and authorize tenant or administrative access.
- Assign a dedicated least-privilege workload identity and use short-lived credentials.
- Retrieve secrets from an approved manager and keep them outside code, images, and logs.
- Use TLS, private dependencies, controlled ingress and egress, and restricted admin routes.
- Enforce request size, schema, rate, concurrency, timeout, retry, and cost limits.
- Verify model and image integrity before startup and minimize container privileges.
- Protect telemetry and audit changes; test credential revocation, backup, rollback, and restore.
service:
min_replicas: 2 # illustrative; choose from tests and objectives
max_replicas: 10
request_timeout_seconds: 5
graceful_shutdown_seconds: 30
concurrency_per_replica: 16
readiness_path: /health/ready
liveness_path: /health/live
resources: {cpu: '1', memory: 1Gi}
autoscaling_signal: concurrent_requests
deployment_strategy: canary11. Monitor the Deployment
| Layer | Signals |
|---|---|
| Traffic and service | Requests, concurrency, errors, timeouts, p50/p95/p99 latency, and releases |
| Resources | CPU, memory, replicas, restarts, saturation, cold starts, and scale events |
| Data | Schema, missingness, ranges, freshness, and feature distributions |
| Model | Prediction distribution, delayed ground-truth MAE, and important slices |
| Product | Usage, human corrections, complaints, and downstream outcomes |
| Security | Authentication failures, limits, unusual access, and artifact changes |
| Economics | Compute, storage, telemetry, and cost per successful prediction |
Correlate telemetry using request, trace, prediction, model, release, schema, and configuration versions without logging raw sensitive features by default. Build an on-call dashboard, an ML-quality dashboard, actionable alerts, and linked runbooks.
12. Release and Roll Back
Approved immutable release
-> staging smoke, contract, security, load, and resilience tests
-> synthetic prediction
-> shadow representative production requests
-> small canary
-> compare errors, latency, saturation, predictions, slices, outcomes, and cost
-> gradually promote or stop and roll back
-> record evidence and reconcile affected predictionsThe rollback target must include compatible model, preprocessing, image, schema, configuration, infrastructure, and policy. After a failed release, identify predictions and downstream decisions it produced and reconcile them according to the runbook.
13. Feedback and Maintenance
- Join verified sale outcomes to prediction IDs after the appropriate delay.
- Review performance by time and important slices without treating incomplete labels as final.
- Quarantine feedback from training until provenance, quality, rights, and representativeness are validated.
- Retrain only when evidence justifies it, then repeat the full evaluation and release process.
- Patch dependencies, rotate secrets, review access, test restoration, right-size resources, and track provider changes.
- Retire the service when unsupported, unowned, unacceptably risky, duplicated, or no longer valuable.
Failure Demonstrations
| Scenario | Expected result |
|---|---|
| Invalid request | Safe 4xx response without model execution |
| Corrupt artifact | Readiness fails and traffic is withheld |
| Traffic spike | Capacity scales within limits and excess load is shed safely |
| Dependency timeout | Deadline and bounded fallback prevent retry storms |
| Model regression | Canary gate stops rollout and restores champion |
| Credential exposure | Credential is revoked, rotated, investigated, and safely redeployed |
| Bad training source | Data is quarantined and cannot produce a release |
| Monitoring outage | Telemetry-gap alert fires and critical audit behavior remains |
| Replica or zone loss | Remaining capacity serves within the chosen objective |
| Retirement | Consumers migrate, traffic stops, evidence remains, and resources are removed |
Cloud Service Mapping
| Capability | AWS | Azure | Google Cloud |
|---|---|---|---|
| Object storage | Amazon S3 | Blob Storage | Cloud Storage |
| Container registry | Amazon ECR | Azure Container Registry | Artifact Registry |
| Managed ML | SageMaker AI | Azure Machine Learning | Vertex AI |
| Managed containers | ECS/Fargate | Container Apps | Cloud Run |
| Kubernetes | Amazon EKS | AKS | GKE |
| Secrets | Secrets Manager | Key Vault | Secret Manager |
| Observability | CloudWatch | Azure Monitor/Application Insights | Cloud Monitoring/Logging |
Implement on one cloud rather than attempting every provider. The architectural duties remain similar even when service names differ. Confirm current regional support, quotas, limits, and pricing before deployment.
Portfolio Deliverables
- README explaining problem, users, architecture, setup, tests, deployment, monitoring, costs, and limitations
- Training, evaluation, service, infrastructure, and pipeline source code
- Data and API contracts, model metadata, evaluation report, and model card
- Reproducible container with dependency locks, scan result, inventory, digest, and provenance
- CI/CD workflow with quality, security, and approval gates
- Load report, dashboard screenshots, alert definitions, and cost estimate
- Evidence of staging, canary promotion, failed-guardrail rollback, and recovery
- Runbook covering incidents, credentials, artifacts, dependencies, feedback, maintenance, and retirement
- Design document explaining choices, alternatives, trade-offs, risks, and future improvements
Common Project Mistakes
| Mistake | Better approach |
|---|---|
| Model committed without lineage | Register immutable artifact with data, source, evaluation, and checksum |
| Only happy-path testing | Add validation, security, load, failure, deployment, and rollback tests |
| Root image and floating dependencies | Use non-root runtime, locks, scanning, digest, and provenance |
| Public unauthenticated API | Use identity, authorization, TLS, network controls, quotas, and safe errors |
| Kubernetes only for résumé value | Choose the simplest target that satisfies measured requirements |
| CPU autoscaling without benchmark | Scale from the actual bottleneck and startup delay |
| Raw property data in logs | Use safe metadata and separately governed prediction records |
| Latest model auto-deployed | Use independent evaluation, approval, canary, and complete rollback |
| Project ends after launch | Include monitoring, feedback, maintenance, incidents, cost, and retirement |
| Claims production readiness without evidence | Publish acceptance criteria, results, limitations, and residual risks |
Final Acceptance Checklist
[ ] Intended use, prohibited use, owner, architecture, objectives, and risks documented
[ ] Data source, contract, split, quality, lineage, privacy, and version controlled
[ ] Model reproducible and compared across baseline, slices, robustness, latency, and cost
[ ] Model, preprocessing, schema, metadata, evaluation, and approval registered immutably
[ ] API typed, versioned, validated, tested, observable, and safely handles errors
[ ] Container non-root, minimal, locked, scanned, provenanced, verified, and benchmarked
[ ] CI/CD tests code, data, model, security, image, contracts, infrastructure, and deployment
[ ] Production uses identity, secrets, TLS, network, request, rate, concurrency, timeout, and cost controls
[ ] Capacity, scaling, shutdown, overload, dependencies, quotas, and unit cost tested
[ ] Service, data, model, outcome, security, and cost monitoring is actionable
[ ] Staging, synthetic, canary, promotion, rollback, restoration, and reconciliation demonstrated
[ ] README, model card, dashboards, alerts, runbook, feedback, maintenance, and retirement complete