Schedule icon
Request icon
Query icon
Script icon
If icon
SlackIncomingWebhook icon
Log icon

Reconcile MLflow Registry Drift Against Deployed Serving Models

Compare MLflow model versions with the production serving registry in Kestra, flag gaps, missing rows, and archived models, and alert Slack with the verdict.

Categories
AIInfrastructure

Model registries and serving tables drift apart quietly. A version gets promoted in MLflow but the deployment job fails, a model gets archived after a rewrite but the old version keeps serving, or a model is registered and never deployed at all. Nobody notices until a rollout references a version that production never ran. This flow reads both sides every day and tells you exactly where they disagree.

How it works

  1. The daily_registry_reconcile trigger (io.kestra.plugin.core.trigger.Schedule, cron 0 5 * * *, shipped disabled: true) starts the check once a day at 05:00 UTC.
  2. fetch_registered_models (io.kestra.plugin.core.http.Request) calls GET /api/2.0/mlflow/registered-models/search on MLFLOW_TRACKING_URI, and fetch_model_versions calls GET /api/2.0/mlflow/model-versions/search for every version together with its current_stage, both authenticated with MLFLOW_TOKEN in an Authorization: Bearer header.
  3. query_deployed_models (io.kestra.plugin.jdbc.postgresql.Query with fetchType: FETCH) reads name, version, and deployed_at from public.deployed_models for the env input, which is what production is actually serving.
  4. reconcile (io.kestra.plugin.scripts.python.Script) picks a target version per model (the latest Production version if one exists, otherwise the latest Staging version) and compares it with the deployed row, then classifies the model as a gap (serving version behind the registered version), missing (registered with a target stage but no serving row), or stale (serving row archived in MLflow, unregistered, or absent from MLflow).
  5. The script emits verdict, gap_count, stale_models, and checked_at through the JSON outputs protocol, plus per category text lists that the alert interpolates directly.
  6. drift_gate (io.kestra.plugin.core.flow.If) branches on needs_attention, which is true when gap_count exceeds registry_gap_tolerance or any model is stale, so the alert path posts to Slack and optionally opens a GitHub issue while the clean path logs an in sync message.

What you get

  • A daily verdict of IN_SYNC, DRIFT, or UNKNOWN with the exact count of models whose serving row lags or is missing.
  • Three explicit lists in Slack: gaps, missing rows, and stale rows, each with the model name and the two versions involved.
  • Stale detection that catches the quiet failure mode where a model keeps serving after MLflow archived or unregistered it.
  • A tolerance input so a known backlog of one or two pending promotions does not page anyone.
  • Flow outputs (gap_count, stale_models, verdict, checked_at) that a parent promotion pipeline can gate on.

Who it's for

  • MLOps engineers who own the promotion path from a registered model to a live serving deployment.
  • Platform teams that keep a serving registry table in Postgres and need a second opinion from MLflow.
  • ML tech leads who want evidence that production runs the version the registry says is current.
  • Auditors and release managers who need a dated record that deployed models were checked against the registry.

Why orchestrate this with Kestra

The comparison itself is a short script, but the reliability is the hard part. Two REST calls need retries, the database read needs credentials that must not live in the script, the branch between alerting and staying quiet must be visible in the execution history, and the failure of the check itself needs its own notification. Kestra gives all of that declaratively, a disabled schedule ships with the flow so you turn it on instead of wiring a cron box, and the outputs turn a daily report into something another flow can branch on. A one-off script scattered across a cron host gives none of that lineage.

Prerequisites

  • A Kestra instance (0.20.0+ recommended) with the core, scripts, Slack, and JDBC PostgreSQL plugins available.
  • An MLflow tracking server with the model registry enabled, reachable from the Kestra worker, without a trailing slash on its base URL.
  • A PostgreSQL database holding public.deployed_models with the columns model_name TEXT, model_version TEXT, environment TEXT, and deployed_at TIMESTAMPTZ.
  • A Slack incoming webhook for the drift alert and for the failure handler.
  • Python available to the script task, or add a Docker task runner to reconcile if the worker has no interpreter.

Secrets

  • MLFLOW_TRACKING_URI: base URL of the MLflow tracking server, for example https://mlflow.example.com with no trailing slash.
  • MLFLOW_TOKEN: bearer token sent in the Authorization header of both registry calls; store a placeholder value if your tracking server does not enforce authentication.
  • POSTGRES_JDBC_URL: JDBC URL of the production database, for example jdbc:postgresql://db.example.com:5432/ml_platform.
  • POSTGRES_USERNAME: database user allowed to read public.deployed_models.
  • POSTGRES_PASSWORD: password for that database user.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL used for the drift alert and the failure alert.
  • GITHUB_TOKEN: only needed when reconciliation_repo is set, so the optional issue can be opened.

Quick start

  1. Add the secrets above to the company.team namespace in Kestra.
  2. Copy this blueprint into flows/mlflow-model-registry-sync.yaml and adjust the namespace if needed.
  3. Confirm the SQL in query_deployed_models matches your serving table, then run the flow once manually and read the verdict from the outputs.
  4. Turn the schedule on by flipping the trigger to the snippet below, or keep it disabled and call the flow from your deployment pipeline.
  5. Set registry_gap_tolerance to the backlog you are willing to carry, and set reconciliation_repo only if you want the follow up issue.
triggers:
  - id: daily_registry_reconcile
    type: io.kestra.plugin.core.trigger.Schedule
    cron: "0 5 * * *"
    disabled: false

Secrets go in Namespace, then Secrets, as MLFLOW_TRACKING_URI, MLFLOW_TOKEN, POSTGRES_JDBC_URL, POSTGRES_USERNAME, POSTGRES_PASSWORD, and SLACK_WEBHOOK_URL, never in this file.

How to extend

  • Change the target stage rule in reconcile so Staging counts as a gap only for specific model name prefixes.
  • Add a second SQL task that writes the verdict into a history table to build a trend of drift over time.
  • Replace the Slack alert with an email or Discord task, or fan out one message per gap with a Loop.
  • Fail the execution on DRIFT with io.kestra.plugin.core.execution.Fail if your release pipeline should stop instead of being notified.
  • Compare against an environment column per stage by running the flow once per env input from a parent flow.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.