Run Spark jobs on Kubernetes from any host using Helm
This article shows how to install Spark operator in a Kubernetes cluster and submit Spark applications to that cluster from any host using Helm and kubectl.
Prerequisites
To deploy Spark operator on Kubernetes, you need:
-
A Kubernetes cluster (1.32 or later) with access configured through
kubectl. -
Helm (3.8.0 or higher) — a package manager that allows quick deployment of Docker images in Kubernetes.
-
Spark artifacts (Docker images and Helm charts) loaded to your private OCI registry. These artifacts can be found in offline packages, which can be requested from the Arenadata support team. To run Spark on Kubernetes, you need to unpack the following images:
-
hub.arenadata.io/adc-enterprise/spark3:<version>
-
hub.arenadata.io/adc-enterprise/spark4:<version>-java17
-
hub.arenadata.io/adc-enterprise/spark4:<version>-java21
-
hub.arenadata.io/ng/charts/spark-apps:<version>
-
hub.arenadata.io/ng/charts/spark-operator:<version>
-
Deployment steps
The steps below describe how to install and configure Spark components on Kubernetes. Configurations related to providing external access, Ingress controllers, load balancers, DNS, and cloud annotations should be performed with respect to your Kubernetes infrastructure.
Step 1. Install Spark operator
First, you have to install Spark operator for Kubernetes, which is responsible for managing the lifecycle of Spark applications in a Kubernetes cluster.
-
Create a Helm values file spark-operator-values.yaml:
spark-operator-values.yaml# Default values for spark-operator. # This is a YAML-formatted file. # Declare variables to be passed into your templates. # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ replicas: 1 payloadNamespaces: (1) # Managed namespaces for Spark payload resources. names: - spark-application-min # Explicit opt-in for cluster-wide RBAC when payloadNamespaces.names is empty. # When false, chart rendering fails until namespaces are specified. allowClusterRole: false deleteProtection: false avoidCreation: false # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: registry: "<registry>" (2) repository: "<image>" (3) # This sets the pull policy for images. pullPolicy: Always # Overrides the image tag whose default is the chart appVersion. tag: "<version>" (4) # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ pullSecret: (5) name: "" ## List of secrets to create for image pulling in all product namespaces credentials: {} # registry: private-docker-registry # username: user # password: pass # This is to override the chart name. nameOverride: "" fullnameOverride: "" # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ serviceAccount: # Automatically mount a ServiceAccount's API credentials? automount: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" # This is for setting Kubernetes Annotations to a Pod. # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ podAnnotations: {} # This is for setting Kubernetes Labels to a Pod. # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ podLabels: {} podSecurityContext: {} # fsGroup: 2000 securityContext: readOnlyRootFilesystem: true privileged: false allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 65532 capabilities: drop: - ALL seccompProfile: type: RuntimeDefault # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ service: # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types type: ClusterIP # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports port: 8443 resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi nodeSelector: {} tolerations: [] affinity: {} terminationGracePeriodSeconds: 10 metrics: # Enable to protect the metrics endpoint with authn/authz. Requires ClusterRole. # See https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.1/pkg/metrics/filters#WithAuthenticationAndAuthorization auth: false1 A list of namespaces for the operator to manage resources. 2 The address of your OCI registry to pull images from. 3 The repository name within your registry. 4 The image version. 5 Credentials to access your private Docker registry. -
Install Spark operator:
$ helm upgrade --install spark-operator oci://<registry-address>/ng/charts/spark-operator:<version> -f spark-operator-values.yaml --namespace spark-operator-min --create-namespacewhere
<registry-address>is the address of your OCI registry with loaded Spark Helm charts.Example output:
Release "spark-operator" does not exist. Installing it now. Pulled: hub.adsw.io/ng/charts/spark-operator:1.41.0 Digest: sha256:cb65eec82abea847af2f4022cd8681e6805f26cab9163bf4b596df8fe4223812 NAME: spark-operator LAST DEPLOYED: Fri Aug 28 08:55:22 2026 NAMESPACE: spark-operator-min STATUS: deployed REVISION: 1 DESCRIPTION: Install complete TEST SUITE: None NOTES:
-
Verify Spark operator installation using the command below:
$ kubectl get pods -n spark-operator-minThe output:
NAME READY STATUS RESTARTS AGE spark-operator-5df74c7458-74zlb 1/1 Running 0 2m17s
Step 2. Deploy a Spark application
Once Spark operator is installed, you have to deploy a test Spark application using a Helm chart.
-
Create spark-submit-values.yaml:
spark-submit-values.yamlimage: registry: "<registry>" (1) repository: "<image>" (2) tag: "<tag>" ## Specify a pullPolicy ## ref: https://kubernetes.io/docs/concepts/containers/images/#pre-pulled-images ## pullPolicy: "Always" ## Existing secret or secret to create to use for image pulling, they must exist in all product namespaces ## pullSecret: (3) name: "" credentials: {} # registry: private-docker-registry # username: user # password: pass #ServiceAccount name for Spark driver/executor pods serviceAccountName: "spark-app" #Application #mainApplicationFile: path to the main app file (hdfs://, local://, etc.) mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.13-3.5.4.4-4.3.0-2.jar" (4) #Restart policy on failure: Never or OnFailure #When set to OnFailure, maxRetries must be specified restartPolicy: "" #Maximum number of Job restart attempts on failure #Required when restartPolicy is OnFailure #maxRetries: #mainClass: set only for JVM apps (e.g. SparkConnectServer) mainClass: "org.apache.spark.examples.SparkPi" #Hadoop configurations hadoopConfigsSecretName: "" #Spark configurations. When set, this client-managed Secret is the complete #Spark configuration source. If Ranger is enabled, include the ranger-spark-*.xml #files in this Secret; the chart does not create a separate Ranger config Secret. sparkConfigsSecretName: "" #Kerberos (keytab mode), disabled by default. #The referenced Secret must already exist and carry two keys: keytab + krb5.conf. #Ticket-cache mode is CLI-only and is not exposed here. #Extra confs (e.g. spark.kerberos.access.hadoopFileSystems) go under sparkConf. kerberos: {} # principal: user@RU-CENTRAL1.INTERNAL # keytab: # secretName: spark-keytab # Arguments passed to the main application class after the main file args: - "10" #Spark configuration #Key/value map rendered into spec.sparkConf sparkConf: {} #SSL / truststore settings. The referenced Secret must already exist; #the operator mounts the stores at /etc/ssl. #Enabled implicitly when secretName is set. ssl: {} # secretName: "ca-store" # trustStoreKey: "truststore.jks" # keyStoreKey: "keystore.jks" #Ranger authorization (Kyuubi Spark Authz plugin), disabled by default. #When enabled without sparkConfigsSecretName, the chart renders the #ranger-spark-*.xml config Secret. The spark-apps.sparkConf helper always appends #the Ranger extension to spark.sql.extensions. #The audit JAAS block is written only when the kerberos block is set (keytab mode); #the policymgr-ssl truststore is rendered only when ssl.enabled. ranger: enabled: false # ranger.plugin.spark.policy.rest.url policyRestURL: "" # ranger.plugin.spark.service.name serviceName: "" # xasecure.audit.destination.solr.zookeepers solrZookeepers: "" job: ## @param replicas set number of job replicas ## replicas: 1 ## When false, the spark-submit Job pod is not automatically deleted after completion (useful for log inspection) ## #deleteOnTermination: false ## Additional configuration that you want to be added to job-config args: {} # "task.max-worker-threads": 8 ## Annotations for job pods annotations: {} ## Set container requests and limits for resource like CPU or memory (essential for production workloads) ## resources: {} #limits: # cpu: "2" # memory: "8Gi" #requests: # cpu: "2" # memory: "8Gi" ## Request additional PVC for pod ## persistentVolume: {} # mountPath: "/data/spark" # volumeClaimTemplates: # - metadata: # name: data # spec: # accessModes: ["ReadWriteOnce"] # resources: # requests: # storage: 10Gi # storageClassName: default ## nodeAffinity: Object defining constraints to place pods on a specific set of Nodes ## nodeSelector: {} topologySpreadConstraints: [] ## Allow a Pod to be scheduled onto nodes that have taints. ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## tolerations: [] # - key: "example-key" # operator: "Exists" # effect: "NoSchedule" ## affinity: Object defining soft rules to place pods on a specific set of Nodes or didn't place pod to nodes due to some conditions ## affinity: {} # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: topology.kubernetes.io/zone # operator: In # values: # - antarctica-east1 # - antarctica-west1 # preferredDuringSchedulingIgnoredDuringExecution: # - weight: 1 # preference: # matchExpressions: # - key: another-node-label-key # operator: In # values: # - another-node-label-value startupProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 livenessProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 readinessProbe: {} # type: exec # command: test -f /opt/spark/etc/truststore/custom-truststore.jks # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 ## Mount additional secrets into the pod ## mountSecrets: [] # - secretName: my-secret # mountPath: /etc/spark/my-secret driver: ## @param replicas set number of driver replicas ## replicas: 1 ## Additional configuration that you want to be added to driver-config args: {} # "task.max-worker-threads": 8 ## Annotations for driver pods annotations: {} ## Set container requests and limits for resource like CPU or memory (essential for production workloads) ## resources: {} #limits: # cpu: "2" # memory: "8Gi" #requests: # cpu: "2" # memory: "8Gi" ## Request additional PVC for pod ## persistentVolume: {} # mountPath: "/data/spark" # volumeClaimTemplates: # - metadata: # name: data # spec: # accessModes: ["ReadWriteOnce"] # resources: # requests: # storage: 10Gi # storageClassName: default ## nodeAffinity: Object defining constraints to place pods on a specific set of Nodes ## nodeSelector: {} topologySpreadConstraints: [] ## Allow a Pod to be scheduled onto nodes that have taints. ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## tolerations: [] # - key: "example-key" # operator: "Exists" # effect: "NoSchedule" ## affinity: Object defining soft rules to place pods on a specific set of Nodes or didn't place pod to nodes due to some conditions ## affinity: {} # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: topology.kubernetes.io/zone # operator: In # values: # - antarctica-east1 # - antarctica-west1 # preferredDuringSchedulingIgnoredDuringExecution: # - weight: 1 # preference: # matchExpressions: # - key: another-node-label-key # operator: In # values: # - another-node-label-value startupProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 livenessProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 readinessProbe: {} # type: exec # command: test -f /opt/spark/etc/truststore/custom-truststore.jks # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 ## Mount additional secrets into the pod ## mountSecrets: [] # - secretName: my-secret # mountPath: /etc/spark/my-secret executor: ## @param replicas set number of executor replicas ## replicas: 1 ## Additional configuration that you want to be added to executor-config args: {} # "task.max-worker-threads": 8 ## Annotations for executor pods annotations: {} ## Set container requests and limits for resource like CPU or memory (essential for production workloads) ## resources: {} #limits: # cpu: "2" # memory: "8Gi" #requests: # cpu: "2" # memory: "8Gi" ## Request additional PVC for pod ## persistentVolume: {} # mountPath: "/data/spark" # volumeClaimTemplates: # - metadata: # name: data # spec: # accessModes: ["ReadWriteOnce"] # resources: # requests: # storage: 10Gi # storageClassName: default ## nodeAffinity: Object defining constraints to place pods on a specific set of Nodes ## nodeSelector: {} topologySpreadConstraints: [] ## Allow a Pod to be scheduled onto nodes that have taints. ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## tolerations: [] # - key: "example-key" # operator: "Exists" # effect: "NoSchedule" ## affinity: Object defining soft rules to place pods on a specific set of Nodes or didn't place pod to nodes due to some conditions ## affinity: {} # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: topology.kubernetes.io/zone # operator: In # values: # - antarctica-east1 # - antarctica-west1 # preferredDuringSchedulingIgnoredDuringExecution: # - weight: 1 # preference: # matchExpressions: # - key: another-node-label-key # operator: In # values: # - another-node-label-value startupProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 livenessProbe: {} # type: httpGet # port: 8080 # path: /v1/info # scheme: HTTP # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 readinessProbe: {} # type: exec # command: test -f /opt/spark/etc/truststore/custom-truststore.jks # initialDelaySeconds: 20 # periodSeconds: 5 # timeoutSeconds: 3 # successThreshold: 1 # failureThreshold: 2 ## Mount additional secrets into the pod ## mountSecrets: [] # - secretName: my-secret # mountPath: /etc/spark/my-secret #Optional: inline Secret for properties-file pattern propertiesFile: enabled: false # Raw content rendered into the Secret's stringData content: "" #RBAC rbac: # Set to false if the ServiceAccount/Role/RoleBinding already exist create: true rules: - apiGroups: - "" resources: - pods - configmaps - persistentvolumeclaims - services - secrets verbs: - get - list - watch - create - update - patch - delete - deletecollection - apiGroups: - networking.k8s.io verbs: - get - list - watch - create - update - patch - delete resources: - networkpolicies1 The address of your OCI registry to pull images from. 2 The name of repository in your registry. 3 Credentials to access your private Docker registry. 4 The path to the Spark application JAR. The local://scheme indicates that the JAR is located inside the Spark container image. -
Deploy the Spark application using Helm:
$ helm upgrade --install spark-application oci://<registry-address>/ng/charts/spark-apps:<version> -f spark-submit-values.yaml --namespace spark-application-min --create-namespaceThe output:
Release "spark-application" does not exist. Installing it now. Pulled: hub.adsw.io/ng/charts/spark-apps:1.41.0 Digest: sha256:73debb0c68945951ec0e0f0ef90ff6f3182184581cfb360157c2b5da47e11cf1 NAME: spark-application LAST DEPLOYED: Fri Aug 28 09:16:19 2026 NAMESPACE: spark-application-min STATUS: deployed REVISION: 1 DESCRIPTION: Install complete TEST SUITE: None
Step 3. Submit Spark application via kubectl
Before submitting a Spark application via kubectl, you have to configure role-based access (RBAC) for the Spark application.
The following manifest includes ServiceAccount, Role, and RoleBinding definitions to provide RBAC details for the Spark application.
-
Create rbac.yaml:
rbac.yaml--- apiVersion: v1 kind: ServiceAccount metadata: name: spark-minimal namespace: spark-kbctl --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: spark-minimal-role namespace: spark-kbctl rules: - apiGroups: - "" resources: - pods - configmaps - persistentvolumeclaims - services - secrets verbs: - get - list - watch - create - update - patch - delete - deletecollection --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: spark-minimal-role-binding namespace: spark-kbctl subjects: - kind: ServiceAccount name: spark-minimal namespace: spark-kbctl roleRef: kind: Role name: spark-minimal-role apiGroup: rbac.authorization.k8s.io -
Create a namespace and apply the configuration:
$ kubectl create namespace spark-kbctl $ kubectl apply -f rbac.yamlThe output:
serviceaccount/spark-minimal created role.rbac.authorization.k8s.io/spark-minimal-role created rolebinding.rbac.authorization.k8s.io/spark-minimal-role-binding created
-
Create spark-app.yaml:
spark-app.yamlapiVersion: spark.arenadata.io/v1alpha1 kind: SparkApplication metadata: labels: app.kubernetes.io/name: spark-operator app.kubernetes.io/managed-by: kustomize name: spark-pi-java namespace: spark-kbctl spec: mainApplicationFile: "local:///opt/spark/examples/jars/spark-examples_2.13-3.5.4.4-4.3.0-2.jar" mainClass: "org.apache.spark.examples.SparkPi" args: - "1000" serviceAccountName: spark-minimal sparkConf: "spark.kubernetes.authenticate.driver.serviceAccountName": "spark-minimal" "spark.kubernetes.namespace": "spark-kbctl" job: spec: image: &image hub.adsw.io/adh-enterprise/spark3-docker:3.5.4.4-adh-4.3.0-x86_64 driver: spec: image: *image executor: replicas: 2 spec: image: *image -
Submit the Spark application via
kubectl:$ kubectl apply -f spark-app.yamlThe output:
sparkapplication.spark.arenadata.io/spark-pi-java created
-
To ensure that the application has succeeded, examine the Spark driver logs in the driver pod:
$ kubectl logs -n spark-application-min spark-appplication-spark-apps-<ID>-driverThe logs reflect major application workflow steps, including the Spark task result — the calculated Pi number.
Pi is roughly 3.1402951402951405