Skip to content

Chart deployment boundaries

These controls describe the current source chart. Published charts, API images and Python packages have independent release histories; inspect the selected chart and image before upgrading. The chart is an API/console deployment with an optional Spark History Server. It does not deploy a Spark scheduler, create a cloud workload identity, or configure applications submitted through the SDK.

Select the image explicitly

The API image uses image.digest when set. Otherwise it uses image.tag; an explicitly empty tag opts into that chart's appVersion fallback. A version tag is clearer than latest, but only a verified digest fixes the image contents.

The source chart requires an explicit compatible image tag or digest while its appVersion is unreleased. Release packaging sets a qualified application version, which becomes the default image tag. Building or rendering the local chart does not build an API image.

The API image contains the API, its locked dependencies and Python 3.14 (0.12.1 used Python 3.11), but no pip, setuptools or wheel. To add packages in an image built FROM it, switch to USER root, run python -m ensurepip to restore the base image's pip, install your packages, then switch back with USER 10001:10001. That pip is the base image's older version, so remove or upgrade it in your image. Alternatively, install the packages in a separate build stage and copy the result.

The optional History Server has a separate image lifecycle. Enabling sparkHistory.enabled requires an explicit sparkHistory.image.tag or sparkHistory.image.digest; there is no implicit API-version or latest fallback. For both images, a digest takes precedence over a simultaneously set tag. Use repository without an embedded tag or digest, and put the complete sha256: plus 64 hexadecimal characters in digest.

No History Server image version is qualified by these chart-rendering checks. Validate your selected History image, event-log storage and Spark runtime together before deployment. An explicitly selected latest tag remains accepted for intentional development use; it does not make the deployment reproducible.

Verify a published API image

The release workflow signs each API image it publishes with Sigstore cosign keyless signing, by digest. The signed digest is the multi-architecture image whose amd64 and arm64 images passed the workflow's checks; both registries receive those bytes without a rebuild, so ghcr.io/hussein-awala/spark-on-k8s and docker.io/husseinawala/spark-on-k8s have the same digest. API images released before signing was added, including 0.12.1 and earlier, have no signature, and cosign verify reports that none was found.

Resolve the release tag once, then verify and deploy the digest it printed, not the tag. Use cosign 3; cosign 2.6 needs --new-bundle-format:

RELEASE_VERSION=X.Y.Z  # replace with the selected API release
docker buildx imagetools inspect "ghcr.io/hussein-awala/spark-on-k8s:$RELEASE_VERSION"
# Set API_IMAGE_DIGEST to the sha256:... value of the Digest line printed above.
: "${API_IMAGE_DIGEST:?Set the digest printed for the release tag}"
cosign verify "ghcr.io/hussein-awala/spark-on-k8s@$API_IMAGE_DIGEST" \
  --certificate-identity "https://github.com/hussein-awala/spark-on-k8s/.github/workflows/release.yml@refs/tags/$RELEASE_VERSION" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Verification succeeds only for a signature made by this repository's release.yml running for that tag; a fork, another workflow, a branch run or a different release tag fails it. Each signature is recorded in Sigstore's public transparency log. Verify the Docker Hub copy with the same command and docker.io/husseinawala/spark-on-k8s@$API_IMAGE_DIGEST. Then pin the verified digest with --set-string image.digest="$API_IMAGE_DIGEST", as in the example below. A per-platform digest is not signed separately; deploy the multi-architecture digest.

The same index carries BuildKit SBOM and provenance attestations for each platform. They are listed by digest in the signed index, so retrieving them through the verified digest also authenticates them:

docker buildx imagetools inspect "ghcr.io/hussein-awala/spark-on-k8s@$API_IMAGE_DIGEST" \
  --format '{{ json .Provenance }}'
docker buildx imagetools inspect "ghcr.io/hussein-awala/spark-on-k8s@$API_IMAGE_DIGEST" \
  --format '{{ json .SBOM }}'

They describe the image contents and build inputs; they do not certify that the dependencies are free of vulnerabilities.

To enforce signatures at admission, configure a Sigstore-capable policy engine to require, for images from these repositories, the issuer https://token.actions.githubusercontent.com and a certificate subject matching ^https://github\.com/hussein-awala/spark-on-k8s/\.github/workflows/release\.yml@refs/tags/[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$. Drop the optional group to refuse pre-releases. The signatures are Sigstore bundles attached as OCI referrers, the format cosign 3 writes; confirm that your engine version verifies that format, and exempt images released before signing.

Start with scoped, externally managed access

The source includes chart/examples/production-values.yaml. It uses explicit analytics,batch namespaces, the preprovisioned spark-observer service account, external RBAC, a ClusterIP service, and illustrative resource reservations. Its REQUIRED_SHA256_DIGEST placeholder deliberately fails validation. Set a digest you have verified for the selected API image repository:

# Set API_IMAGE_DIGEST to the verified sha256:... digest; no sample image is implied.
: "${API_IMAGE_DIGEST:?Set the verified API image digest}"
helm lint ./chart -f chart/examples/production-values.yaml \
  --set-string image.digest="$API_IMAGE_DIGEST"
helm template spark-console ./chart --namespace spark-system \
  -f chart/examples/production-values.yaml \
  --set-string image.digest="$API_IMAGE_DIGEST" > rendered-console.yaml

Provision the release namespace and named service account before installation. The administrator must grant that account the needed access in the selected application namespaces. Review the rendered resources and adjust the example's namespaces, resource requests and limits for your deployment. The example does not establish availability, throughput or cloud-identity compatibility.

The default readOnly: true remains unchanged. With chart-managed RBAC, the default empty namespaces creates a Role and RoleBinding in the release namespace, and an explicit namespace string creates them in each listed namespace. namespaces: "*" instead creates a ClusterRole with the same rules and binds it cluster-wide; the chart does not bind the built-in view or edit roles. The chart passes the same scope to the API as SPARK_ON_K8S_API_NAMESPACES, so the API refuses other namespaces even where Service DNS would reach them. Neither the namespace scope nor hiding UI buttons provides user or tenant authorization; use the remote-submission security boundary.

Decide who owns the account and permissions

rbac.create controls API roles and bindings independently of serviceAccount.create. Its default, null, follows serviceAccount.create to preserve existing rendering. The History Server has its own service-account setting; API RBAC settings do not grant History Server storage permissions.

API configuration Account created by Helm Roles/bindings created by Helm
serviceAccount.create: true, rbac.create: null Yes Yes; legacy default
serviceAccount.create: false, rbac.create: null No No; legacy external-account behavior
serviceAccount.create: false, explicit serviceAccount.name, rbac.create: true No Yes, bound to that account in the release namespace
serviceAccount.create: true, rbac.create: false Yes No
serviceAccount.create: false, rbac.create: false No No; fully external ownership

The chart requires an explicit account name when creating RBAC for an external account. readOnly only changes chart-managed permissions; it cannot reduce permissions already granted externally.

Review the chart-managed permissions

Chart-managed Roles and the cluster-wide ClusterRole render the same rules, all on core API resources in the served namespaces:

readOnly Rules Used for
true get and list on pods, get on pods/log, list on events Application lists and details, driver logs, diagnostics, readiness and run history
false Adds create and delete on pods, get and create on pods/exec, and create on services Stopping and deleting drivers, and remote submission

These are the only verbs the API calls: it never watches pods, the Spark UI proxy reaches Services by DNS, and a driver's Service is deleted with the driver through its owner reference. Grant more through externally managed RBAC if other tools use the API's service account.

Neither mode grants Secrets, ConfigMaps or any other resource. Remote submission creates only a driver Pod and its headless Service, so readOnly: false covers it; the API rejects jobs that would create Secrets or ConfigMaps. Spark drivers create executors with the job's own service account, not the API's; see cluster prerequisites for a baseline for that account.

With namespaces: "*", readOnly: false lets the API create, delete and exec into pods in every namespace, including system namespaces. Rendering fails unless rbac.allowClusterWideWrite: true acknowledges that grant; listing the namespaces the API manages keeps write access scoped instead. Read-only cluster-wide access and externally managed RBAC need no acknowledgement.

When changing ownership on an installed release, inspect the Helm diff first. Setting a create flag to false removes previously managed resources from the release; Helm can delete them during upgrade. Arrange the replacement permissions and deliberate resource-ownership transfer before that upgrade. Do not assume that changing the flag alone preserves the existing account or grants.

Keep the four workload identities separate

Role Identity and metadata inputs Image and image-pull inputs
API/console Chart serviceAccount.*, podLabels, podAnnotations; its Kubernetes credentials observe/manage applications Chart image.*, imagePullSecrets
Spark driver Submission namespace, service_account, driver_labels, driver_annotations and driver template Submission image, image_pull_policy; driver template for supported pull-secret customization
Spark executors Application executor template and Spark configuration for the chosen runtime; executor_labels and executor_annotations Application image/configuration and executor template; API chart pull secrets are not inherited
History Server Chart sparkHistory.serviceAccount.*, sparkHistory.podLabels, sparkHistory.podAnnotations; independent event-storage identity sparkHistory.image.*; chart imagePullSecrets is applied to its pod too

These roles may share an account when that is intentional; they do not need to. The resolved submission service_account overrides a service account written inside the driver template. API chart account annotations and labels do not become application defaults. See driver templates for the supported merge rules and inspect the generated driver plan.

Cloud IAM bindings, storage credentials and admission policies remain owned by the cluster administrator. A successful doctor check establishes only its reported caller checks and account existence; it does not prove that a driver, executor or History Server can access your object store. Cloud-specific recipes and arbitrary-user container support are not qualified by this chart validation suite.

Run the API as a non-root user

The API image runs as the dedicated user and group 10001. The chart's default podSecurityContext and securityContext match that identity and meet Pod Security restricted: runAsNonRoot, runAsUser, runAsGroup and fsGroup 10001, fsGroupChangePolicy: OnRootMismatch, the RuntimeDefault seccomp profile, no privilege escalation and all capabilities dropped. Override individual keys in your values file, or set a key to null to remove it; an image built with another user needs matching IDs. The History Server keeps its own security settings.

The API must be able to read files mounted from volumes, such as a remote-submission token or policy, or a kubeconfig selected with SPARK_ON_K8S_CONFIG_FILE. The chart sets SPARK_ON_K8S_IN_CLUSTER=true unless env sets SPARK_ON_K8S_IN_CLUSTER or SPARK_ON_K8S_CONFIG_FILE, so a selected kubeconfig is used instead of the pod's service account. Its home directory is /home/spark-on-k8s; /root is not readable. Keep service.port at 1024 or above, because some container runtimes do not let a non-root process bind lower ports. Mount a remote-submission Secret as a directory rather than with subPath, so a token rotation reaches running pods.

A run-history claim written by an earlier root API process holds root-owned files. On volume types that support fsGroup, such as most block-storage CSI drivers and local volumes, the kubelet makes them group-writable at the first non-root mount. Volume types that ignore fsGroup, such as hostPath-backed provisioners, keep them read-only: the API then exits with attempt to write a readonly database. Before upgrading such a claim, change ownership from the current root pod:

kubectl exec --namespace <release-namespace> deploy/<release-fullname> -- \
  chown -R 10001:10001 /var/lib/spark-on-k8s

If the upgraded API already exited, helm rollback to the root release and run the command there before upgrading again. To keep running as root instead, restore the previous identity and capabilities; the capabilities let root write files that a failed non-root start may have created. Such a pod does not meet Pod Security restricted:

podSecurityContext:
  runAsNonRoot: false
  runAsUser: 0
  runAsGroup: 0
securityContext:
  capabilities: null

readOnlyRootFilesystem is not a default because the Kubernetes client writes temporary files in some configurations, such as kubeconfig credentials embedded as data. To enable it, also mount a writable /tmp:

securityContext:
  readOnlyRootFilesystem: true
volumes:
  - name: tmp
    emptyDir: {}
volumeMounts:
  - name: tmp
    mountPath: /tmp

Understand the scaling controls

Control What it scales Owner
Chart replicaCount or autoscaling.* API/console replicas API deployment owner
Application ExecutorInstances / Spark dynamic allocation Executors within one Spark application Application owner
Kubernetes cluster/node-pool autoscaler Available worker nodes Cluster/cloud administrator

Increasing autoscaling.maxReplicas does not increase an application's executor ceiling or create nodes. The optional History Server has its own sparkHistory.replicaCount and autoscaling settings. CPU/memory HPA targets need appropriate container requests and the cluster's metrics support; setting a target to null or 0 disables that metric, and an enabled HPA requires at least one positive target.

The metadata recorder is separate from the Spark History Server. Enabling runHistory requires exactly one API replica, no API autoscaling, an existing writable persistent claim, and one to twenty explicit namespace entries. Unless namespaces is "*", every runHistory.namespaces entry must also be in the API scope: the release namespace by default, or the listed namespaces. Otherwise the API refuses to start, and with chart-managed RBAC rendering fails. The chart uses a Recreate rollout. It neither provisions the claim nor validates its filesystem locking behavior; use supported block/local storage, rehearse backups and allow for rollout downtime.

Validate before installation

values.schema.json checks chart-owned types, image references, service ports, namespace forms, HPA bounds and recorder settings. Template guards cover cross-field comparisons, such as the cluster-wide write acknowledgement, and reserved volume names. Run both helm lint and helm template: lint can suppress template-level fail messages, while rendering rejects those cross-field conflicts. namespaces remains a comma-separated string; runHistory.namespaces is a YAML list. Kubernetes passthrough objects, such as affinity, resources and security contexts, remain open so normal cluster-specific fields are accepted.

Rendering checks do not validate cloud permissions, image availability, storage access or API server admission. Raw env overrides still take precedence over chart-generated environment settings and remain the operator's responsibility. An ingress.tls entry terminates TLS before the API, so browser log streams are rejected until env sets SPARK_ON_K8S_API_WEBSOCKET_ALLOWED_ORIGINS to the public https:// origins; see browser origins. To serve the API below an ingress path such as /platform/spark, have the ingress controller remove that prefix before forwarding and set SPARK_ON_K8S_API_ROOT_PATH to the same path in env; see mounted paths. To require single sign-on, keep the chart Ingress disabled and publish the console through the oauth2-proxy gateway recipe instead. Use the development guide for the Helm regression suite and release guidance for the artifact publication boundary.

Add operational objects

The settings in this section are disabled by default, apart from the History Server probes, which apply only when sparkHistory.enabled is set. A release upgraded with --reuse-values from a chart without these settings renders as before. Some invalid combinations fail only during rendering, so run helm template as well as helm lint.

Restrict API traffic

networkPolicy.enabled adds a NetworkPolicy for the API pods. It admits traffic to the API port only from networkPolicy.ingress.from and from pods with this release's API labels: the API pods and the chart's helm test pod. With an empty list nothing else can connect, including an ingress controller, a gateway, Prometheus and clients of a LoadBalancer or NodePort Service. List every peer that needs the API:

networkPolicy:
  enabled: true
  ingress:
    from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: ingress-nginx
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: monitoring
        podSelector:
          matchLabels:
            app.kubernetes.io/name: prometheus

The policy needs a network plugin that enforces NetworkPolicy; other plugins accept the object and enforce nothing. NetworkPolicy does not block traffic from a pod's own node, so kubelet probes keep working. Traffic through a LoadBalancer or NodePort Service can arrive with a node address rather than the client's, depending on externalTrafficPolicy and the plugin. A NetworkPolicy limits which peers can connect; it does not authenticate callers or replace remote mode or an authenticating gateway.

networkPolicy.egress.enabled also restricts the API's outbound connections to:

  • DNS on port 53 to networkPolicy.egress.dns.to: kube-dns pods in kube-system by default. An empty list allows port 53 to any destination, for example a node-local DNS cache;
  • the Kubernetes API server peers in networkPolicy.egress.kubernetesApi.to, on kubernetesApi.ports (443 and 6443 by default);
  • port 4040 on pods labeled spark-role=driver in the namespaces selected by namespaces, for the Spark UI proxy;
  • this release's API pods, so its helm test pod can connect, and the History Server pods on port 18080 when sparkHistory.enabled is set;
  • networkPolicy.egress.extraRules, for example a cluster selected by SPARK_ON_K8S_CONFIG_FILE.

The chart cannot discover API server addresses. Most plugins evaluate egress after a Service address is translated to its endpoints, so list the endpoint addresses of the default/kubernetes Service:

kubectl get endpointslices --namespace default \
  --selector kubernetes.io/service-name=kubernetes \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\n"}{end}'
networkPolicy:
  egress:
    enabled: true
    kubernetesApi:
      to:
        - ipBlock:
            cidr: 172.18.0.2/32

With an empty kubernetesApi.to, the chart's policy grants no API server access: the API stays unready unless another policy allows it, and the install notes warn about it. Plugins differ: some match the Service address before translation, and some do not match node or control-plane addresses with ipBlock at all and need their own policy resources for the API server. After enabling egress, check readiness, the Spark UI proxy and log streams. The driver namespace selector follows namespaces; an env override of SPARK_ON_K8S_API_NAMESPACES does not change it.

sparkHistory.networkPolicy.enabled adds an ingress-only NetworkPolicy for the History Server: the API pods and sparkHistory.networkPolicy.ingress.from can reach port 18080. History Server egress, such as to event-log storage, stays unrestricted.

Keep replicas available during node drains

podDisruptionBudget.enabled adds a PodDisruptionBudget for the API pods, and sparkHistory.podDisruptionBudget.enabled one for the History Server pods. Set minAvailable or maxUnavailable to a pod count or a percentage such as "50%". Setting both is rejected; setting neither uses maxUnavailable: 1.

Rendering fails when a budget permits no voluntary disruption at the smallest replica count, which is replicaCount, or autoscaling.minReplicas with autoscaling enabled. Such a budget blocks kubectl drain, node pool upgrades and cluster autoscaler scale-down indefinitely. For example, minAvailable: 1 with one replica is rejected; Kubernetes rounds percentages up, so minAvailable: "60%" with two replicas is rejected too. Run at least two API replicas to keep the API available while a node drains. A single-replica API, as run history requires, can only use a budget that allows its pod to be evicted, so a budget adds no protection there.

Scrape metrics with the Prometheus Operator

metrics.serviceMonitor.enabled adds a monitoring.coreos.com/v1 ServiceMonitor for the API's /metrics and requires metrics.enabled. Rendering fails if the cluster does not serve the ServiceMonitor API, rather than silently omitting the object and leaving monitoring unconfigured. helm template and client-side dry runs do not query a cluster, so pass the API explicitly:

helm template spark-console ./chart --namespace spark-system \
  --set metrics.enabled=true --set metrics.serviceMonitor.enabled=true \
  --set-string image.tag="$API_IMAGE_TAG" \
  --api-versions monitoring.coreos.com/v1/ServiceMonitor

Set metrics.serviceMonitor.labels to match your Prometheus serviceMonitorSelector, and interval or scrapeTimeout to override the Prometheus defaults. The API and History Server Services share the chart's selector labels, so the ServiceMonitor keeps only the API Service's targets by name. With networkPolicy.enabled, also list Prometheus in networkPolicy.ingress.from. Run-history metric values come from the run-history database, so every API worker reports the same values. The operation counters are kept by each worker instead, and each scrape reaches one worker: set workers: 1 before alerting on them.

With remote.enabled, /metrics requires the operator bearer token, and the ServiceMonitor authenticates with the remote.tokenKey entry of remote.existingSecret. The Prometheus Operator then copies that token into the configuration of every Prometheus instance that selects the ServiceMonitor. The token grants full operator access to the API, including submissions, logs, kill and delete: restrict access to that Prometheus configuration accordingly, or do not enable the ServiceMonitor in remote mode.

Prometheus sends that Secret entry as the bearer token byte for byte, while the API trims whitespace and accepts one token per line. The entry must therefore hold exactly one token with no trailing newline, or every scrape fails. During a token rotation the entry holds two tokens, so scrapes fail from step 1 until step 3 reaches the pods: expect a gap in these metrics and pause alerts on failed scrapes for that time.

Set the API worker count

workers sets SPARK_ON_K8S_API_WORKERS, the number of API processes in each pod, from 1 to 64. The default null keeps the image default, which is 4 in the published image. An env entry with the same name takes precedence. Each worker is a separate process with its own memory, Kubernetes client and SPARK_ON_K8S_API_LIMIT_CONCURRENCY connection limit, so raise resources with the worker count. Run history keeps its single replica but supports several workers in that pod: a database lease elects one collector. In remote mode, each worker reads the token and policy files when it starts.

With more than one worker, spark-on-k8s api start exits with code 0 when a worker fails to start, for example on an unusable run-history volume or an invalid setting: uvicorn's process supervisor logs Child process [...] failed to start and stops without reporting the failure as an exit code. The chart's readiness probe on /ready keeps such a pod out of service, and Kubernetes restarts the container. Read that log line, or run with workers: 1 to get exit code 3, when diagnosing a pod that never becomes ready.

Probe the History Server

With sparkHistory.enabled, the History Server container has readiness and liveness probes on its UI port, 18080, at /api/v1/version. Readiness keeps a starting server out of its Service. Liveness restarts a server that has stopped answering for about two minutes, after a one-minute initial delay. Adding the probes changes the pod template, so upgrading a release with the History Server enabled restarts its pods once.

Override individual probe fields under sparkHistory.readinessProbe and sparkHistory.livenessProbe, or set a probe to null to remove it. Helm merges your probe with the default, so set httpGet: null beside another handler such as tcpSocket. An authentication filter on the History Server UI must allow the probe path, or the probe must use tcpSocket.

Enable remote submission

remote.enabled configures remote submission. The chart sets SPARK_ON_K8S_SUBMISSION_ENABLED, SPARK_ON_K8S_SUBMISSION_TOKEN_FILE and SPARK_ON_K8S_SUBMISSION_POLICY_FILE, and mounts only the remote.tokenKey and remote.policyKey entries of remote.existingSecret read-only under remote.mountPath (/etc/spark-on-k8s/remote by default). The chart never creates this Secret and has no value for an inline token, so the token stays out of Helm values and release history. Create the Secret in the release namespace with your secret tooling, for example from a random token file and the policy:

kubectl create secret generic spark-api-remote --namespace spark-system \
  --from-file=token=./submission-token --from-file=policy.json=./submission-policy.json
remote:
  enabled: true
  existingSecret: spark-api-remote
namespaces: analytics
readOnly: false

Every route except /health and /ready then requires the bearer token, including the console; kubelet probes and the chart's helm test use those two public routes. Submissions create driver pods and Services, so with chart-managed RBAC set readOnly: false. Keep the policy's namespaces within namespaces: the API refuses to start with a policy namespace outside its scope. A missing Secret key also prevents the pod from starting. The Secret is mounted as a directory, so a token rotation or policy change reaches running pods after the kubelet's sync period; each worker then applies valid files without a restart. env entries for the same variables take precedence, and remote-submission is a reserved volume name. The token is one shared operator credential, not tenant isolation.

Preparing the next chart release

The source chart marks appVersion as unreleased and requires an explicit compatible image.tag or image.digest. The release publisher sets the package's qualified application version; an empty image tag then selects that version. API 0.12.1 does not implement /ready and is not compatible with these probes. Do not publish this chart against that older API image. The helm-release workflow refuses an app_version below 0.13.0 or without a published ghcr.io/hussein-awala/spark-on-k8s image, and a chart_version that the chart repository already lists. Run it only after the package release has published the API image, selecting that release tag in the workflow's ref selector so the chart source matches the qualified application.

When upgrading a release installed from published chart 0.3.0, pass your values file again, or use --reset-then-reuse-values (Helm 3.14 and later), instead of --reuse-values. --reuse-values replaces this chart's defaults with the old release's values, including its latest image tags, and omits new settings such as rbac, runHistory and metrics. The chart schema rejects such an upgrade with a missing properties error before rendering.

Shared RBAC resource names now include the release namespace identity. Existing installations can set rbac.legacyNames: true to retain their old names during an upgrade. New installations should use the default false, allowing identical release names in separate namespaces. To migrate existing installations, render and review both RBAC sets, ensure the new bindings exist before removing old bindings, and verify access under the service account. Switching the value in a normal Helm upgrade replaces the old resources; schedule that transition if a brief permissions interruption is unacceptable. Never reuse legacy names for two releases managing the same shared scope.

Releases with namespaces: "*" were bound to the built-in view or edit ClusterRole by a ClusterRoleBinding named after the release with a -view or -edit suffix. The chart now creates its own ClusterRole and a ClusterRoleBinding with the role's name and no suffix, whether or not rbac.legacyNames is set. Because a binding's roleRef cannot change, the binding takes a new name instead of being updated in place: the same helm upgrade creates the new role and binding and removes the old binding, so no manual deletion is needed. As with the name change above, schedule it if a brief permissions interruption is unacceptable. The API's account loses everything else those roles granted, such as reading ConfigMaps and, with edit, reading and writing Secrets in every namespace. If another workload shares that account and relies on such access, grant it separately before upgrading. A "*" release with readOnly: false must also set rbac.allowClusterWideWrite: true; without it, the upgrade fails before changing anything.

The default namespaces value changed from "*" to empty, which selects only the release namespace. A release that relied on the old default loses its cluster-wide binding on upgrade, and the API then refuses other namespaces. Set namespaces: "*" or list the namespaces before upgrading. With "*", the console opens the API's own namespace unless SPARK_ON_K8S_API_DEFAULT_NAMESPACE is set in env. If env sets SPARK_ON_K8S_API_DEFAULT_NAMESPACE to a namespace outside the new scope, the API refuses to start and helm upgrade --wait times out: include that namespace in namespaces, or change or remove the variable, in the same upgrade.

History configuration updates now roll its Deployment automatically. History affinity and tolerations are passed to the Pod. The rare long-name suffix collision is disambiguated; existing installations using those colliding names need a reviewed recreation because Deployment selectors are immutable.