Skip to content

About

Local MLOps platform: gated blue/green releases with measured automatic rollback on kind

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ModelLaunch

CI Python 3.12 License: MIT Ruff mypy strict

Local MLOps platform that packages, deploys, monitors and rolls back an ML inference service on a single-node kind cluster. An engineer supplies a model bundle and runs one release command; the platform validates the candidate against a fixed release-validation set, deploys it blue/green behind a preview Service, runs correctness and load gates, promotes it, watches Prometheus for the verification window and rolls back automatically if latency or error thresholds are breached.

Headline results

Measured on Apple M5, 24 GB, macOS 26.6.2 (arm64), kind v0.33.0 go1.27.0 darwin/arm64, Kubernetes v1.37.0; 2 replicas; paced load at 100 req/s from 8 clients; thresholds from benchmarks/thresholds.yaml. Every number below is read from the JSON records by modellaunch summarize.

Run Outcome Measured
20260913T234851Z-release-tiny-1.0.0 Released 165 s from validate to stable, 12 gates passed; accuracy 0.943 >= 0.9; preview p95 4.8 ms; watch p95 4.5 ms over 24 Prometheus samples
20260913T235450Z-rejected-tiny-1.1.0 Rejected at validating accuracy 0.425 against a 0.9 gate on the fixed release-validation set; no image built, no pods created, stable stays tiny-1.0.0
20260913T235238Z-rollback-tiny-1.2.0 Automatic rollback candidate turned slow 45 s after start (slow fault); p95 868 ms breached the 250.0 ms gate on 2 consecutive samples; tiny-1.0.0 serving again 874 ms after detection (fault to detection 15.3 s at 5 s scrape granularity)
20260913T235451Z-release-flowbench-0.1.0 Released (FlowBench CNN), replacing tiny-1.0.0 155 s from validate to stable, 12 gates passed; relative_l2 0.466 <= 0.55; preview p95 25.7 ms; watch p95 23.8 ms over 24 Prometheus samples

Failure drills (make drills, run 20260913T235137Z-drills), written up in docs/INCIDENT_REPORT.md:

Drill Result Measured
Kill one replica under continuous load passed Deleted inference-tiny-1-0-0-566fcb77b7-2msgg at t=15.0s under 94.58 req/s. 0 of 5675 requests failed (none). p95 before/10 s after kill/steady: 6.048 / 7.576 / 7.493 ms. Readiness returned to 2/2 3.0s after the delete.
Automatic rollback after a latency fault passed tiny-1.2.0 was promoted, then p95 rose to 868 ms after the 45 s fault delay; breach confirmed on 2 consecutive samples (15291 ms after the fault was first observed). Stable reverted to tiny-1.0.0; first healthy response 874 ms after detection. Status: rolled_back.
Invalid input to /predict passed 7 malformed bodies all answered 422 with a JSON pointer message; /live 200, /ready 200, a valid request 200 afterwards. Prometheus 422 counter rose by 7.
Missing weights in a bundle passed Release validation rejected the bundle in 0 ms (artifact_integrity: BundleMissingFileError: bundle file 'model.onnx' is missing from artifacts/bundles/tiny/1.0.0-broken.1). Deployed anyway: the container ran (liveness ok, 0 restarts) but never became ready; the readiness gate timed out after 31.0s and the candidate was deleted. The preview Service had no endpoints.
Failed quality gate passed Candidate tiny-1.1.0 rejected at validating: quality_accuracy 0.425 >= 0.9. Images for it before/after: 0/0; inference deployments before/after: 1/1; stable stays tiny-1.0.0.

Grafana dashboard provisioned from JSON, under 100 req/s of paced load against the released FlowBench model (make bench, p95 22 ms client-side over 8,715 requests):

Grafana dashboard

Release console

The three records replay in a static console: site/index.html, published on GitHub Pages at https://abdulalimamnun.github.io/modellaunch/ once Pages is enabled for the repository. It has a record picker, the six-step gate pipeline, the blue/green route panel, the p95 and error-rate chart drawn from the Prometheus samples against the declared gates, the event log and the release contract, all read from the committed records. The playback speed control compresses the replay and is labelled as such; every number on the page is a recorded measurement. Query parameters select a record and jump to its end, e.g. index.html?run=rollback&end=1.

Quickstart

Requires uv, colima, docker, kind and kubectl (brew install colima docker kind kubectl uv, then colima start --cpu 4 --memory 8 --disk 40). Python 3.12 is pinned in .python-version. Verified from a fresh clone on 2026-09-13.

make setup                                   # uv sync --all-extras + pre-commit hooks
make lint test                               # ruff, mypy --strict, 141 unit tests (about 7 s)
make train-tiny train-tiny-degraded train-tiny-next   # three bundles, about 1 s each on CPU

The inference image serves the bundle with onnxruntime behind FastAPI: /live as soon as the process binds, /ready only after the bundle hash is verified and a warm-up prediction has run (0.7 s after docker run on this machine), /predict validated against the bundle's JSON Schema, /metrics for Prometheus. Full command list, environment variables and the FlowBench packaging steps: docs/OPERATIONS.md.

make up                                                     # kind cluster + Services + Prometheus + Grafana, about 60 s
make release MODEL=artifacts/bundles/tiny/1.0.0             # gated release, about 2.5 min including the 120 s watch
make release MODEL=artifacts/bundles/tiny/1.1.0 SCENARIO=rejected
make release MODEL=artifacts/bundles/tiny/1.2.0 SCENARIO=rollback FAULT_MODE=slow FAULT_DELAY_S=45
make drills                                                 # five failure drills, writes docs/INCIDENT_REPORT.md (about 4 min)
make export-site                                            # newest record per scenario into site/records/
curl -s localhost:30080/ready                               # stable Service via NodePort
open http://localhost:30030                                 # Grafana dashboard, provisioned from JSON
make down

Each release writes releases/<run_id>.json (schema: releases/schema.json). Every number in it comes from the bundle check, the load generator, kubectl or Prometheus. Thresholds are declared in benchmarks/thresholds.yaml before any run.

A bundle directory holds manifest.json, model.onnx, preprocess.json, schemas/request.json, schemas/response.json and evaluation.json. The manifest records name, semantic version, training-code git revision and a SHA-256 over the payload files; identical training runs produce identical hashes. Release gates use the committed release-validation set in benchmarks/validation/ against the thresholds declared in benchmarks/thresholds.yaml; the final test split is never read by the platform.

Design decisions with rejected alternatives: docs/DECISIONS.md.

Architecture

flowchart LR
    subgraph host [Host: Python 3.12 via uv]
        B[bundle: manifest, ONNX, schemas, hash] --> K[k8s wrapper: docker, kind, kubectl]
        R[release workflow: state machine, gates, record] --> REC[(releases/*.json)]
        L[load generator] --> R
    end
    subgraph cluster [kind cluster in Colima]
        S[(inference-stable :30080)] -. selector .-> BLUE[Deployment vN, 2 replicas]
        P[(inference-preview :30081)] -. selector .-> GREEN[Deployment vN+1, 2 replicas]
        PROM[Prometheus :30090] -. scrape .-> BLUE
        PROM -. scrape .-> GREEN
        PROM --> GRAF[Grafana :30030]
    end
    K -- build, load, apply --> GREEN
    R -- selector patch --> S
    R -- selector patch --> P
    R -- PromQL --> PROM
    REC --> SITE[site/: release console]
Loading

Details, the state machine and the bundle and record contracts: docs/ARCHITECTURE.md. Every command, copy-pasteable: docs/OPERATIONS.md. A two-minute walk-through: docs/DEMO.md.

Design notes

  • The workflow enforces the gates, not Kubernetes. Readiness probes decide whether a pod gets traffic; the quality, correctness, load and watch gates decide whether a version does. Promotion and rollback are a selector patch on one Service, which is why both take a few hundred milliseconds.
  • The serving image never imports PyTorch. Bundles carry an ONNX graph plus a declarative preprocess spec; the 96 MB image runs onnxruntime behind FastAPI and is ready 0.7 s after start.
  • Identity without a registry. The image tag encodes the bundle's SHA-256 and the platform's git revision; the Docker image ID and the pod's runtime image ID are recorded. Deployments never reference :latest.
  • Thresholds were declared before any run in benchmarks/thresholds.yaml, are copied into every record, and are the same numbers the Prometheus alert rules and the Grafana threshold lines use (tests check all three agree).
  • The release-validation set belongs to the platform. A candidate is scored on benchmarks/validation/<model>.npz, never on anything it ships, and never on a model's final test split.
  • Honest timing. Rollback duration is breach detection to first healthy response from the previous version on a fresh connection; the time from fault to detection is reported separately with its 5 s scrape granularity. Keep-alive clients follow old pods until they reconnect, and the records say so.
  • Every non-obvious choice, with the alternative rejected: docs/DECISIONS.md.

Status

Phase Scope State
1 Scaffold, bundle format, tiny model, unit tests done
2 Inference service (/predict, /live, /ready, /metrics), container done
3 kind cluster and manifests done
4 Release workflow and release records done
5 Prometheus and Grafana as code done
6 Load tests, failure drills, incident report done
7 Release console (GitHub Pages) done
8 FlowBench checkpoint released through the same workflow done
9 Fresh-clone reproduction, README polish, demo script done

License

MIT.

About

Local MLOps platform: gated blue/green releases with measured automatic rollback on kind

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages