Migrate from the Spark Operator¶
Teams running the Kubeflow Spark Operator describe applications as
SparkApplication and ScheduledSparkApplication custom resources. Since 0.13.0,
spark-on-k8s job assess-operator assesses those manifests offline and drafts
equivalent job files for the settings that translate.
What changes: who owns the application¶
Read this before comparing YAML. The operator is a controller: it watches custom resources, submits them, restarts or resubmits them according to their restart policy, runs schedules and deletes finished applications after a time to live. spark-on-k8s is a client. It creates the driver pod, Service and ConfigMaps, then returns or observes the run when asked. Nothing reconciles the application afterwards.
| Concern | Spark Operator | spark-on-k8s |
|---|---|---|
| Submission | Controller runs spark-submit in cluster mode |
Client creates the driver pod directly; Spark runs in client mode inside it |
| Retries | restartPolicy resubmits |
The caller retries; each retry is a new submission (automation) |
| Schedules | ScheduledSparkApplication |
A Kubernetes CronJob or Airflow |
| Spark UI | Operator-created Service or Ingress | ui_reverse_proxy: true with the API's Spark UI proxy, or kubectl port-forward |
| Cleanup | timeToLiveSeconds |
apps prune on a schedule you own, or app delete; prune ages count from pod creation, not completion; the driver pod owns executors and client-created objects |
| Driver pod shape | Operator webhook or pod template | Driver pod templates and job-file fields |
Keep the operator for workloads that depend on controller behavior, such as automatic resubmission after node loss. Both can run in the same cluster while you migrate; the assessor never reads or changes cluster resources.
Assess a manifest¶
spark-on-k8s job assess-operator spark-pi.yaml
spark-on-k8s job assess-operator manifests.yaml --output json > assessment.json
spark-on-k8s job assess-operator spark-pi.yaml --draft-dir drafts/
The input can contain several YAML documents. Each
sparkoperator.k8s.io/v1beta2 SparkApplication or ScheduledSparkApplication
is assessed; other documents, including older API versions, are listed as skipped.
Files are limited to 1 MiB, and YAML aliases or duplicate keys are rejected.
Every field in an assessed document receives one status. A finding for an object or list covers the fields nested below it.
| Status | Meaning |
|---|---|
mapped |
The draft carries the setting with the same meaning, or the SparkJob default already behaves the same way. target names the SparkJob setting. |
approximated |
The draft carries it, or its intent is covered differently; note states the difference. Review each one. |
unsupported |
Not carried into the draft. note says why and alternative recommends what to use instead. Unrecognized fields are always unsupported: nothing is translated by guesswork. |
ignored |
Observed state or server-written metadata (status, metadata.uid), unmounted volumes and settings that have no effect in the source either. |
An application is migration_ready only when it has no unsupported findings and
its draft passes offline validation. A restart policy that resubmits, a schedule,
or an operator-only feature such as monitoring therefore always produces
migration_ready: false. Readiness is an assessment result, not a guarantee that
the job behaves identically: compare a staging run before switching production.
Each application also lists notes for differences that are not tied to one
field, such as the lifecycle change above, a missing namespace (job files default
to default), a missing driver service account (job files default to spark) or
unset memory overhead (job files default to 512 MiB per pod).
Output and exit codes¶
--output json writes one document to stdout:
{
"schema_version": 1,
"operation": "assess_operator",
"error": null,
"migration_ready": false,
"applications": [
{
"document": 1,
"kind": "SparkApplication",
"name": "spark-pi",
"namespace": "spark-jobs",
"migration_ready": false,
"counts": {"mapped": 20, "approximated": 6, "unsupported": 1, "ignored": 0},
"findings": [
{
"path": "spec.restartPolicy",
"status": "unsupported",
"target": null,
"note": "spark-on-k8s submits once; no controller resubmits ...",
"alternative": "Retry from the caller ..."
}
],
"notes": ["Lifecycle ownership changes: ..."],
"draft": {"apiVersion": "spark-on-k8s/v1", "kind": "SparkJob", "...": "..."},
"draft_error": null,
"draft_file": null
}
],
"skipped": [{"document": 2, "api_version": "v1", "kind": "ConfigMap", "reason": "..."}]
}
Paths use dots for fields, [N] for list positions and [key] for map keys such
as spec.sparkConf[spark.executor.memory]. Consumers should check
schema_version, tolerate additional fields and avoid parsing the text report.
| Exit code | Meaning |
|---|---|
0 |
Every document was read and assessed, including when fields are unsupported or migration_ready is false |
1 |
Unreadable or invalid input (invalid YAML, no Spark Operator application, a document without metadata.name or spec), or a draft file could not be written; the JSON result has an operation_error |
2 |
Invalid CLI arguments, including a missing input file or a --draft-dir that is an existing file |
Use migration_ready or the counts to gate a migration in CI; the exit code
does not.
Drafts¶
A draft is a spark-on-k8s/v1 SparkJob document containing only the mapped and
approximated settings. The command builds it, then validates and plans it in
process exactly like spark-on-k8s job validate, without contacting Kubernetes.
Only a draft that passes is emitted; otherwise draft is null and draft_error
explains the validation failure (for example, no image in the source).
--draft-dir DIRECTORY (created if missing) writes each valid draft to DIRECTORY/NAME.yaml with a
header comment marking it as a draft. Existing files are never replaced, and
nothing is written when any destination already exists. The JSON output always
contains the draft object.
spark-on-k8s job assess-operator spark-pi.yaml --draft-dir drafts/
spark-on-k8s job validate drafts/spark-pi.yaml
spark-on-k8s job plan drafts/spark-pi.yaml --output json
The draft name is the source name, converted to a valid SparkJob name when
needed. Generated application IDs add a timestamp suffix, so driver pods are no
longer named NAME-driver. A ScheduledSparkApplication produces a draft of its
spec.template; schedule it separately.
Credentials stay out of reports and drafts¶
Findings never contain source values. A setting that looks like a credential is
reported as unsupported and left out of the draft, using the same detection as
plan redaction: sensitive key or variable names (password, token,
secret.key, access.key and similar), credentials in URLs, signed URL
parameters and NAME=secret tokens in arguments or Java options. Move such
values to Kubernetes Secrets and reference them, for example with
spark.kubernetes.executor.secretKeyRef.NAME for executors and a
valueFrom.secretKeyRef entry in the driver pod template. Detection is
pattern-based: review drafts before committing them, because positional arguments
or unusual names can still contain sensitive data.
How fields translate¶
The report is the authoritative accounting for a given manifest. The main translations are:
| Spark Operator field | SparkJob setting |
|---|---|
metadata.name, metadata.namespace |
metadata.name, spec.namespace |
metadata.labels |
driver_labels and executor_labels, except kueue.x-k8s.io/ labels, which the operator does not copy to pods (unsupported) |
image, driver.image, executor.image |
image; a different executor image becomes spark.kubernetes.executor.container.image |
imagePullPolicy, imagePullSecrets |
image_pull_policy; pull secrets in the driver template and spark.kubernetes.container.image.pullSecrets |
mainApplicationFile, mainClass, arguments |
app_path, class_name, app_arguments |
sparkConf, hadoopConf |
spark_conf (hadoopConf keys gain the spark.hadoop. prefix) |
Driver and executor cores, memory, memoryOverhead |
driver_resources and executor_resources (whole cores, MiB) |
executor.instances, dynamicAllocation |
executor_instances |
deps.packages, deps.jars, deps.files, deps.pyFiles, deps.archives |
packages; spark.jars, spark.files, spark.submit.pyFiles, spark.archives |
serviceAccount |
service_account (driver); spark.kubernetes.authenticate.executor.serviceAccountName (executors) |
nodeSelector, labels, annotations |
driver_* and executor_* maps |
env, envFrom, volumeMounts, configMaps, secrets, tolerations, affinity, security contexts, sidecars, initContainers |
driver_pod_template and executor_pod_template; executor literals and Secret keys use spark.executorEnv.* and spark.kubernetes.executor.secretKeyRef.*, and executor Secret mounts use spark.kubernetes.executor.secrets.* |
Notable approximations and unsupported fields:
mode,type,sparkVersionandpythonVersionhave no SparkJob field: the image andapp_pathdetermine them.- The driver's CPU request and limit both equal
driver_resources.cpuin whole cores, sodriver.coreRequestanddriver.coreLimitare approximated. Executor values becomespark.kubernetes.executor.request.coresandlimit.cores. - A driver sidecar that keeps running keeps the driver pod, and any waiting, running. Prefer a native sidecar; see driver pod templates.
gpuandhostNetworkare unsupported: add GPU limits or host networking to the pod templates yourself, then verify the running driver and executor pods.- Dependencies given as local paths are unsupported: the driver resolves them in
its own pod. Bake them into the image and use
local://, or use a remote URI. sparkConfkeys that shape a cluster-mode driver pod (spark.kubernetes.driver.secrets.*,.volumes.*,.podTemplateFileand similar) are unsupported; driver labels, annotations, node selectors, resources and the driver service account are translated into SparkJob fields instead.restartPolicyother thanNever, schedules,timeToLiveSeconds,sparkUIOptions,driverIngressOptions,monitoring,batchScheduler,sparkConfigMap,hadoopConfigMap,proxyUserand operator podtemplatefields are unsupported. The recipes cover scheduling, batch schedulers, lineage and object storage.
Limits¶
The assessor is not a controller, importer of live resources or parity checker.
It reads files only, does not read the cluster or the operator's configuration
(for example webhook defaults), and does not compare a draft with pods the
operator created. Volumes, sidecars and other pod fields are copied into
templates without a complete Kubernetes schema check; the API server still
validates them at submission. To trial a migration, submit the validated draft in
a staging namespace next to the existing application and compare its output, pod
specifications from job plan and resource use before retiring the custom
resource.