Riadh Mnasri
← Back to blog
4 min read

Apache Spark on Kubernetes and Azure for computing risk indicators

In the article on VaR, I mention that these risk calculations run every night across a bank's entire portfolio, and must finish before markets open. That's exactly the kind of constraint, massive simulation plus a strict deadline, that justifies assembling Spark, Kubernetes and Azure, rather than stacking these three building blocks by technological reflex.

Why Spark, specifically, for this calculation

A Monte Carlo simulation for a VaR or CVA involves replaying thousands of possible market paths across every position in a portfolio, then aggregating the result. It's an "embarrassingly parallel" problem: each scenario is computed independently of the others, which maps exactly onto Spark's partitioning model.

Spark propertyWhat it brings to this calculation
Partitioning by scenario/positionEach executor computes a subset of paths in parallel
In-memory computation (DataFrame/RDD)Avoids re-reading the position reference data at each aggregation step
Fault tolerance via lineageLosing an executor doesn't restart the whole batch, just its partition
Distributed aggregationsThe final quantile calculation (the VaR itself) happens after collecting partial results

Lineage-based fault tolerance deserves emphasis: Spark doesn't checkpoint state at every step, it remembers how to reconstruct a lost partition from the source data. On a batch running several hours overnight, losing a node only costs recomputing its share of the work, not starting over.

Why Kubernetes as the cluster manager

Spark has supported Kubernetes natively since version 2.3, alongside YARN and Mesos. The concrete benefit: the risk computation cluster runs on the same platform as the rest of the services (APIs, market data ingestion), without operating a separate Hadoop/YARN cluster just for this batch.

                        Driver pod
                   ┌─────────────────┐
                   │  plans the DAG,  │
                   │  collects        │
                   │  results         │
                   └────────┬────────┘
                            │ creates
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
       Executor pod   Executor pod   Executor pod
       (scenarios     (scenarios     (scenarios
        1-1000)        1001-2000)     2001-3000)

The driver dynamically creates executor pods via the Kubernetes API, one per slice of work, then tears them down once the computation finishes. In practice, this is declared through the Kubeflow Spark Operator, which introduces a SparkApplication custom resource rather than invoking spark-submit by hand:

apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: var-nightly-batch
spec:
  type: Scala
  mode: cluster
  image: registry.example.com/risk-engine:1.4.0
  driver:
    cores: 2
    memory: 4g
  executor:
    cores: 4
    memory: 8g
    instances: 50

The trap specific to Kubernetes: dynamic allocation

On YARN, dynamic allocation (adjusting the number of executors on the fly based on load) relies on an external shuffle service that persists even after an executor shuts down. Kubernetes doesn't natively offer that equivalent. Since Spark 3.0, the solution is shuffle tracking: Spark keeps an executor alive as long as it holds shuffle data used by an active job, with no dependency on an external service.

spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.shuffleTracking.enabled=true

Without this option explicitly enabled, dynamic allocation on Kubernetes doesn't behave as expected: executors can be removed while still holding shuffle data needed by a later step of the computation.

What Azure actually changes

On AKS, two choices have a direct impact on this kind of batch:

AspectChoice relevant to a risk batch
Storing market data and positionsAzure Data Lake Storage Gen2, accessed through the abfss:// connector, rather than storage local to the nodes
Driver/executor identity toward storageMicrosoft Entra Workload ID (see the article on Kubernetes day to day), no static storage key in an environment variable
Nodes for the overnight batchA dedicated Spot node pool: the overnight window tolerates preemption, since a lost partition is automatically recomputed thanks to Spark's lineage

The Spot pool is the most counterintuitive choice at first glance, but the most consistent with the Spark properties described above: lineage-based fault tolerance turns a constraint (nodes that can disappear at any time) into an occasional slowdown, not a batch failure. A service that must respond in real time doesn't have that latitude; a batch with several hours before markets open does.

What I don't recommend by default: operating Spark on AKS yourself

Databricks (natively available on Azure) handles most of this operational complexity: dynamic allocation, autoscaling, Spark version upgrades, without having to operate the Spark Operator or Kubernetes manifests yourself. Choosing a self-hosted Spark on AKS is justified when a specific constraint demands it (multi-cloud portability, fine-grained control over the Spark version or native dependencies, cost at very large scale where a managed service's margin adds up), not as a default option. For most teams computing risk indicators, Databricks remains the more reasonable starting point; Spark on Kubernetes becomes worthwhile when that operational simplicity is knowingly traded for control that the managed option doesn't allow.