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

Output
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 back

Success Criteria

AreaAcceptance example
ModelCandidate beats a documented baseline without critical slice regression
APIValid requests return typed results and invalid requests receive safe client errors
PerformanceRepresentative p95 latency and throughput meet chosen objectives
ReliabilityHealth checks, bounded concurrency, graceful shutdown, and rollback work
SecurityNo embedded secrets, non-root image, restricted identity, validation, and authenticated production access
ObservabilityTraffic, errors, latency, model version, prediction distribution, and cost are visible
ReproducibilityData, source, dependency, model, image, schema, and configuration versions are traceable
OperationsOwnership, limitations, deployment, incident, recovery, maintenance, and retirement are documented

Repository Structure

Output
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.md

1. 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.

YAML
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-team

2. 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.

Python
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.
Output
[ ] 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 complete

4. Define Typed API Schemas

Python
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

Python
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

Python
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
TestCoverage
Unit and modelTransformations, baseline, slices, boundaries, and reproducibility
ContractSchemas, status codes, health, version, and errors
IntegrationArtifacts, secrets, gateway, telemetry, and deployment target
SecurityIdentity, authorization, validation, limits, secrets, and image scan
LoadThroughput, p95/p99 latency, memory, concurrency, and cost
ResilienceDependency timeout, overload, restart, node loss, and graceful shutdown
ReleaseReadiness, synthetic prediction, canary comparison, and rollback

7. Containerize

DOCKERFILE
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"]
Terminal
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

YAML
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

TargetGood fitTrade-off
Managed model endpointIntegrated model registry, autoscaling, and prediction operationsPlatform conventions and pricing
Serverless containerStateless service with compatible startup and resource needsCold starts and platform limits
Managed container serviceFlexible APIs and workers without Kubernetes operationsLess low-level orchestration control
KubernetesExisting platform or complex serving genuinely requires itHighest operational complexity
Virtual machineSpecialized runtime or learning environmentTeam 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.
YAML
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: canary

11. Monitor the Deployment

LayerSignals
Traffic and serviceRequests, concurrency, errors, timeouts, p50/p95/p99 latency, and releases
ResourcesCPU, memory, replicas, restarts, saturation, cold starts, and scale events
DataSchema, missingness, ranges, freshness, and feature distributions
ModelPrediction distribution, delayed ground-truth MAE, and important slices
ProductUsage, human corrections, complaints, and downstream outcomes
SecurityAuthentication failures, limits, unusual access, and artifact changes
EconomicsCompute, 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

Output
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 predictions

The 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

ScenarioExpected result
Invalid requestSafe 4xx response without model execution
Corrupt artifactReadiness fails and traffic is withheld
Traffic spikeCapacity scales within limits and excess load is shed safely
Dependency timeoutDeadline and bounded fallback prevent retry storms
Model regressionCanary gate stops rollout and restores champion
Credential exposureCredential is revoked, rotated, investigated, and safely redeployed
Bad training sourceData is quarantined and cannot produce a release
Monitoring outageTelemetry-gap alert fires and critical audit behavior remains
Replica or zone lossRemaining capacity serves within the chosen objective
RetirementConsumers migrate, traffic stops, evidence remains, and resources are removed

Cloud Service Mapping

CapabilityAWSAzureGoogle Cloud
Object storageAmazon S3Blob StorageCloud Storage
Container registryAmazon ECRAzure Container RegistryArtifact Registry
Managed MLSageMaker AIAzure Machine LearningVertex AI
Managed containersECS/FargateContainer AppsCloud Run
KubernetesAmazon EKSAKSGKE
SecretsSecrets ManagerKey VaultSecret Manager
ObservabilityCloudWatchAzure Monitor/Application InsightsCloud 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

MistakeBetter approach
Model committed without lineageRegister immutable artifact with data, source, evaluation, and checksum
Only happy-path testingAdd validation, security, load, failure, deployment, and rollback tests
Root image and floating dependenciesUse non-root runtime, locks, scanning, digest, and provenance
Public unauthenticated APIUse identity, authorization, TLS, network controls, quotas, and safe errors
Kubernetes only for résumé valueChoose the simplest target that satisfies measured requirements
CPU autoscaling without benchmarkScale from the actual bottleneck and startup delay
Raw property data in logsUse safe metadata and separately governed prediction records
Latest model auto-deployedUse independent evaluation, approval, canary, and complete rollback
Project ends after launchInclude monitoring, feedback, maintenance, incidents, cost, and retirement
Claims production readiness without evidencePublish acceptance criteria, results, limitations, and residual risks

Final Acceptance Checklist

Output
[ ] 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