From DevOps to MLOps: Airflow’s Hybrid Executor and KServe on Kubernetes

Modern MLOps looks a lot like DevOps—pipelines, containers, CI/CD—but with new wrinkles: GPUs, data freshness, and model serving. If you already run Airflow on Kubernetes for data engineering, there’s a timely path to productionizing ML: combine Airflow 2.10’s Hybrid Executor and Dataset improvements with KServe’s CRD-based model serving on your Kubernetes cluster. KServe entered CNCF as an incubating project in September 2025, and its v0.15 release added LLM‑oriented autoscaling, caching, and a gateway integration—making it a strong fit for both classic and generative ML serving. (cncf.io)

What changed recently—and why you should care

Reference architecture

A minimal training-to-serving DAG

Below is a compact pattern that:

from datetime import datetime
from airflow.decorators import dag, task
from airflow.datasets import Dataset
from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator

FEATS = Dataset("s3://my-ml-bucket/feature-table.parquet")

@dag(
    dag_id="train_and_deploy_kserve",
    start_date=datetime(2024, 1, 1),
    schedule=[FEATS],    # data-aware scheduling
    catchup=False,
    default_args={"executor": "KubernetesExecutor"},  # route tasks to K8s by default
)
def pipeline():

    @task(map_index_template="-")
    def train(hparams: dict) -> dict:
        # Your training code (runs in a pod with KubernetesExecutor)
        # Return metrics + model URI
        return {"metric": 0.9, "model_uri": f"s3://my-ml-bucket/models/{hparams['lr']}-{hparams['depth']}"}

    # Fan-out: hyperparameter sweep with readable map labels in UI (Airflow 2.9+)
    trials = train.expand(hparams=[
        {"lr": 0.01, "depth": 6},
        {"lr": 0.02, "depth": 8},
        {"lr": 0.05, "depth": 10},
    ])

    @task
    def select_best(results: list[dict]) -> str:
        best = max(results, key=lambda r: r["metric"])
        return best["model_uri"]

    best_uri = select_best(trials)

    # Deploy the winner via a tiny kubectl container
    deploy = KubernetesPodOperator(
        task_id="deploy_to_kserve",
        name="kserve-apply",
        image="bitnami/kubectl:1.30",
        cmds=["/bin/sh", "-c"],
        arguments=[r"""
cat <<'EOF' | kubectl apply -n ml -f -
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: churn-model
spec:
  predictor:
    model:
      modelFormat:
        name: mlflow
      storageUri: 
EOF
"""],
        get_logs=True,
    )

    deploy.set_upstream(best_uri)

pipeline()

KubernetesExecutor vs. KubernetesPodOperator in ML pipelines

A practical rule: start with KubernetesExecutor for Python-heavy TaskFlow jobs; switch to KubernetesPodOperator for GPU jobs or language-specific containers.

Operations checklist

Takeaway

If your team already knows DevOps on Kubernetes, you’re close to MLOps. Airflow 2.10’s Hybrid Executor and dataset-centric scheduling give you clean control over when and where ML work runs; KServe turns a trained artifact URI into a versioned, autoscaled endpoint. Start with the minimal pattern above, then layer in GPU nodepools, per‑task images, and KEDA policies as your workload grows. (airflow.apache.org)