Kubernetes model operator¶
The OpenMed Kubernetes operator makes the model version served by an existing OpenMed REST Deployment declarative. An OpenMedModel resource selects one family, version pointer, tier, replica count, and rollout policy. The operator then:
- writes an owned ConfigMap containing the active model manifest pointer;
- points the target container's
OPENMED_SERVICE_PRELOAD_MODELSvariable at that ConfigMap; - hashes the manifest into the Deployment pod template so Kubernetes replaces the pods and each new process warms the selected model before
/readyzsucceeds; - reports rollout state through standard conditions and Kubernetes Events;
- restores the last successful pointer when a rollout exceeds the Deployment progress deadline and automatic rollback is enabled.
The operator does not orchestrate training, change cluster autoscaling, ship model weights, or contact a model registry. Its only network dependency is the Kubernetes API. The service remains responsible for resolving the configured model pointer from its local cache or an explicitly permitted model source.
Resource contract¶
OpenMedModel is namespaced under openmed.ai/v1alpha1. Its required fields are:
| Field | Meaning |
|---|---|
spec.family | Logical model family, such as PII. |
spec.version | Exact model name, immutable repository id, or local path accepted by the OpenMed service. |
spec.tier | One of Tiny, Small, Medium, Base, Large, XLarge, or Accurate-XLarge. |
spec.replicas | Target Deployment replica count, from 1 through 1000. |
spec.rolloutStrategy | RollingUpdate or Recreate, plus deadline and rollback controls. |
spec.targetRef.name defaults to the resource name and spec.targetRef.containerName defaults to openmed-service. The target must be an existing Deployment in the same namespace. One OpenMedModel owns one target Deployment; a second resource targeting an already owned Deployment is held with a TargetConflict condition rather than racing the first resource.
The CRD rejects unknown spec fields and tiers before reconciliation. The operator repeats the same validation internally so malformed resources cannot bypass the contract through an older API server or direct test invocation.
Build and install¶
Build and publish the small operator image to a registry available to the cluster. Kopf is the only operator-specific dependency and is pinned in the image; it is also available through the operator Python extra.
docker build \
-f deploy/operator/Dockerfile \
-t registry.example/openmed-operator:v2.3.0 \
.
docker push registry.example/openmed-operator:v2.3.0
Set the image in deploy/operator/deployment.yaml (or with a Kustomize image override), then install the CRD, RBAC, namespace, and single-replica operator:
kubectl apply -k deploy/operator
kubectl -n openmed-system rollout status deployment/openmed-operator
The bundled Deployment runs one replica with a Recreate strategy. Running multiple standalone Kopf replicas against the same resources can cause duplicate reconciliation, so do not scale it horizontally. The operator has a liveness endpoint but no readiness endpoint because it does not serve workload traffic.
For local development without building the image:
uv sync --extra operator
uv run kopf run --standalone --all-namespaces \
deploy/operator/openmed_operator.py
Prepare the service Deployment¶
Install the OpenMed REST service first. Its Deployment must expose the standard openmed-service container and /readyz probe. For the Helm chart, use a stable name matching the custom resource and leave initial preload selection to the operator:
helm upgrade --install openmed-service deploy/helm/openmed-service \
--namespace openmed \
--create-namespace \
--set fullnameOverride=openmed-service \
--set-json 'config.preloadModels=[]'
Production clusters should retain the chart's persistent model cache. Ensure the model named by spec.version is already cached for an air-gapped rollout, or explicitly configure the service pod with the credentials and egress needed for its one-time download. The operator never reads those credentials and its RBAC grants no access to Secrets.
Apply and observe a model¶
The synthetic example targets the Helm Deployment above:
kubectl apply -f deploy/operator/example-openmedmodel.yaml
kubectl -n openmed get openmedmodel openmed-service -w
kubectl -n openmed wait \
--for=condition=Ready \
--timeout=10m \
openmedmodel/openmed-service
Inspect the value-free lifecycle evidence with:
kubectl -n openmed describe openmedmodel openmed-service
kubectl -n openmed get events \
--field-selector involvedObject.kind=OpenMedModel
kubectl -n openmed get configmap openmed-service-model-manifest -o yaml
The ConfigMap contains only family, tier, version pointer, and the preload environment value. The status stores those coordinates, hashes, Deployment generations, and retained successful specs. It never contains request text, detected entities, model output, patient identifiers, or credentials.
Roll out a new version¶
Change spec.version to an immutable challenger pointer:
kubectl -n openmed patch openmedmodel openmed-service \
--type=merge \
-p '{"spec":{"version":"OpenMed/synthetic-pii-v2"}}'
The first reconcile writes the new pointer and sets Progressing=True with phase RollingOut. New pods preload the model before their service readiness probe passes. Once the Deployment has observed its new generation and all desired replicas are updated, ready, and available, the operator sets Ready=True, records the version as lastSuccessfulSpec, and emits a RolloutSucceeded Event.
The operator continuously reconciles every 15 seconds. If Helm or another controller removes the manifest reference, replica count, or rollout settings, the operator reports DriftCorrected and restores the custom resource's desired state.
Automatic and manual rollback¶
With rollbackOnFailure: true, a Deployment condition of ProgressDeadlineExceeded or ReplicaFailure=True causes an automatic pointer flip to lastSuccessfulSpec. The resource remains explicit about the mismatch:
status.phaseisRolledBack;status.desiredVersionremains the failed version;status.activeVersionis the restored version;Degraded=Trueand, once the restored pods are available,RolledBack=Truewith reasonRollbackSucceeded.
The operator does not retry the same failed spec forever. Change the spec to a new version to start another rollout, or set it to the restored version to make desired and active state agree.
After two versions have succeeded, request a deliberate rollback to either of the retained successful versions with an annotation:
kubectl -n openmed annotate openmedmodel openmed-service \
openmed.ai/rollback-to=OpenMed/synthetic-pii-v1 --overwrite
Only lastSuccessfulSpec and previousSuccessfulSpec are accepted manual targets. This prevents an annotation from loading an unreviewed model. After the rollback is available, update spec.version to the restored pointer and remove the annotation:
kubectl -n openmed patch openmedmodel openmed-service \
--type=merge \
-p '{"spec":{"version":"OpenMed/synthetic-pii-v1"}}'
kubectl -n openmed annotate openmedmodel openmed-service \
openmed.ai/rollback-to-
Conditions and Events¶
| Condition | Interpretation |
|---|---|
Ready | The requested version is available on every desired replica. |
Progressing | Kubernetes is rolling out either the desired or rollback pointer. |
Degraded | The target is missing/conflicted, the rollout failed, or desired state is held at a rollback. |
RolledBack | A retained successful version has been fully restored. |
The operator emits idempotently named Events for rollout start/success/failure, rollback start/success/rejection, missing/conflicting targets, and deletion. Event messages contain lifecycle state only and are safe for cluster-level operational logs.
Deletion lifecycle¶
Deleting an OpenMedModel removes its ConfigMap pointer and changes the target container's preload value to an empty string. The pod-template hash changes, so the target Deployment replaces its pods and shuts down the old warm pool. The operator never deletes model weights or the target Deployment. The ConfigMap also has an owner reference as a garbage-collection backstop.
RBAC and namespace scope¶
The bundled deployment watches all namespaces. Its ClusterRole can:
- watch and patch
OpenMedModelresources, status, and finalizers; - read and patch Deployments;
- manage only ConfigMaps and Events used for reconciliation;
- discover the CRD for Kopf.
It cannot read Secrets, create workloads, mutate Services, change autoscalers, or access nodes. Clusters that need strict tenant isolation can copy these same rules into a namespaced Role, replace --all-namespaces with one or more --namespace arguments, and bind the service account only in those namespaces.
Offline synthetic validation¶
The unit suite starts an in-process fake Kubernetes HTTP API, applies a synthetic OpenMedModel, observes ConfigMap and Deployment patches, advances a healthy rollout, forces a progress-deadline failure, and verifies restoration of the last successful pointer. It also validates the CRD and deployment/RBAC manifests:
No cluster, model download, restricted vocabulary, real patient data, or external network call is required by these tests.
Troubleshooting¶
TargetNotFound: the target Deployment name or namespace does not match the custom resource. Setspec.targetRef.nameexplicitly.ContainerNotFound: setspec.targetRef.containerNameto the OpenMed REST container name.TargetConflict: anotherOpenMedModelalready owns the target Deployment. Give each resource a separate target.ProgressDeadlineExceeded: inspect pod events, cache capacity, model pointer, credentials, memory limits, and/readyz. Automatic rollback starts only when a last successful spec exists.RollbackTargetUnavailable: manual rollback retains only the current and immediately previous successful specs.
Model extraction is assistive software, not a source of clinical truth. A model rollout must not automatically trigger diagnosis, treatment, billing, data release, or another clinical decision.