Spark on Kubernetes — Production-Grade Deployment

Bienvenue dans ce module où tu vas apprendre à déployer Apache Spark sur Kubernetes. C’est l’architecture moderne de référence pour exécuter des workloads Spark en production.


Prérequis

Niveau Module Compétence
✅ Requis Module 14 Docker Fundamentals
✅ Requis Module 15 Kubernetes Fundamentals
✅ Requis Module 16 Kubernetes for Data Workloads
✅ Requis Module 19 PySpark Advanced
✅ Requis Module 20 Spark SQL Deep Dive
💡 Recommandé - Expérience avec kubectl et Helm

Objectifs du module

À la fin de ce module, tu seras capable de :

  • Comprendre l’architecture Spark on Kubernetes
  • Construire une image Docker Spark optimisée
  • Configurer les ressources Kubernetes (RBAC, Secrets, PVC)
  • Lancer des jobs avec spark-submit sur K8s
  • Utiliser Spark Operator pour la production
  • Configurer l’autoscaling (Dynamic Allocation, KEDA)
  • Mettre en place le monitoring (Spark UI, Prometheus, Grafana)
  • Debugger les erreurs courantes

1. Introduction — Pourquoi Spark sur Kubernetes ?

1.1 L’évolution de l’infrastructure Spark

2010s                              2020s+
┌─────────────────┐                ┌─────────────────┐
│   Hadoop/YARN   │                │   Kubernetes    │
│   ┌─────────┐   │                │   ┌─────────┐   │
│   │  Spark  │   │    ────────▶   │   │  Spark  │   │
│   └─────────┘   │                │   └─────────┘   │
│   On-premise    │                │   Cloud-native  │
└─────────────────┘                └─────────────────┘

1.2 Avantages de Kubernetes

Avantage Description
Cloud-native Même infra que le reste de tes applications
Multi-cloud AWS, GCP, Azure, on-premise
Isolation Namespaces, RBAC, Network Policies
Autoscaling HPA, VPA, KEDA, Cluster Autoscaler
CI/CD Images Docker versionnées, GitOps
Cost optimization Spot instances, autoscaling down
Standardisation Plus besoin d’expertise Hadoop/YARN

1.3 Support officiel

  • Spark 2.3 : Support expérimental K8s
  • Spark 3.0 : Support stable (GA)
  • Spark 3.1+ : Améliorations (PVC, Pod templates)
  • Spark 3.4+ : External shuffle service sur K8s

1.4 Comparaison YARN vs Kubernetes

Aspect YARN Kubernetes
Écosystème Hadoop-centric Cloud-native, polyglot
Scheduling ResourceManager kube-scheduler
Isolation Containers/cgroups Pods, Namespaces, Network Policies
Scaling Manual ou scripts HPA, VPA, KEDA, Cluster Autoscaler
Packaging JAR/ZIP sur HDFS Images Docker
Secrets Hadoop credentials K8s Secrets, Vault
Monitoring YARN UI, Ganglia Prometheus, Grafana, native
Multi-tenancy Queues Namespaces + RBAC
Data locality ✅ Excellent ⚠️ Limité (shuffle coûteux)
Courbe d’apprentissage Hadoop stack K8s stack
Adoption 2024+ Legacy Standard moderne
📊 Quand choisir quoi ?

YARN si :                           Kubernetes si :
├── Cluster Hadoop existant         ├── Infrastructure cloud-native
├── Data locality critique          ├── Multi-cloud ou hybrid
├── Équipe Hadoop experte           ├── CI/CD moderne (GitOps)
└── Pas de migration prévue         └── Autoscaling avancé requis

2. Architecture Spark on Kubernetes

2.1 Vue d’ensemble

┌─────────────────────────────────────────────────────────────────┐
│                        KUBERNETES CLUSTER                       │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                     Namespace: spark                      │  │
│  │                                                           │  │
│  │   ┌─────────────────┐      ┌─────────────────────────┐   │  │
│  │   │   Driver Pod    │      │     Executor Pods       │   │  │
│  │   │  ┌───────────┐  │      │  ┌───────┐ ┌───────┐   │   │  │
│  │   │  │  Spark    │  │◀────▶│  │ Exec  │ │ Exec  │   │   │  │
│  │   │  │  Driver   │  │      │  │  #1   │ │  #2   │   │   │  │
│  │   │  └───────────┘  │      │  └───────┘ └───────┘   │   │  │
│  │   │  - Spark UI     │      │  ┌───────┐ ┌───────┐   │   │  │
│  │   │  - Coordinateur │      │  │ Exec  │ │ Exec  │   │   │  │
│  │   └─────────────────┘      │  │  #3   │ │  #4   │   │   │  │
│  │           │                │  └───────┘ └───────┘   │   │  │
│  │           │                └─────────────────────────┘   │  │
│  │           ▼                                               │  │
│  │   ┌─────────────────┐      ┌─────────────────────────┐   │  │
│  │   │  ConfigMaps     │      │       Secrets           │   │  │
│  │   │  - spark-config │      │  - cloud credentials    │   │  │
│  │   └─────────────────┘      └─────────────────────────┘   │  │
│  │                                                           │  │
│  │   ┌─────────────────────────────────────────────────────┐│  │
│  │   │              PersistentVolumeClaims                 ││  │
│  │   │  - checkpoints  - logs  - shuffle (optional)       ││  │
│  │   └─────────────────────────────────────────────────────┘│  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

2.2 Rôles et responsabilités

Composant Géré par Responsabilité
Pods scheduling Kubernetes Placement sur les nodes
Networking Kubernetes Communication Driver ↔︎ Executors
Secrets Kubernetes Credentials cloud/DB
Storage Kubernetes PVC pour checkpoints/logs
Driver Spark Coordination du job
Executors Spark Exécution des tasks
Shuffle Spark Échange de données entre stages

2.3 Client Mode vs Cluster Mode

CLIENT MODE                              CLUSTER MODE
┌──────────────┐                        ┌──────────────────────────────┐
│ Local Machine│                        │      Kubernetes Cluster      │
│  ┌────────┐  │                        │  ┌────────┐                  │
│  │ Driver │  │◀─────────┐             │  │ Driver │◀────┐            │
│  └────────┘  │          │             │  │  Pod   │     │            │
└──────────────┘          │             │  └────────┘     │            │
                          │             │        ▲        │            │
┌─────────────────────────┼─────────┐   │        │        │            │
│      K8s Cluster        │         │   │        ▼        ▼            │
│  ┌────────┐ ┌────────┐  │         │   │  ┌────────┐ ┌────────┐      │
│  │Executor│ │Executor│◀─┘         │   │  │Executor│ │Executor│      │
│  │  Pod   │ │  Pod   │            │   │  │  Pod   │ │  Pod   │      │
│  └────────┘ └────────┘            │   │  └────────┘ └────────┘      │
└───────────────────────────────────┘   └──────────────────────────────┘
Aspect Client Mode Cluster Mode
Driver Machine locale Pod Kubernetes
Usage Développement, debug Production
Spark UI localhost:4040 Via Service/Ingress
Réseau Doit atteindre le cluster Interne au cluster
Résilience ❌ Si local crash → job perdu ✅ Pod peut être reschedulé
CI/CD ❌ Difficile ✅ Natif

💡 Règle : Client mode pour le dev/debug, Cluster mode pour la production.

Exercice 1 : Identifier les composants

Réponds aux questions suivantes :

  1. Dans quel mode le Driver tourne-t-il en tant que Pod K8s ?
  2. Qui gère le scheduling des Executor Pods ?
  3. Où sont stockés les credentials cloud ?
💡 Voir les réponses
  1. Cluster mode — Le Driver est un Pod K8s
  2. Kubernetes (kube-scheduler) — Spark demande des Pods, K8s les place
  3. Kubernetes Secrets — Montés dans les Pods Driver/Executor

3. Construire une Image Docker Spark

3.1 Image de base

Plusieurs options :

Image Avantage Inconvénient
apache/spark Officielle Basique
bitnami/spark Bien maintenue, non-root Taille moyenne
gcr.io/spark-operator/spark Pour Spark Operator Spécifique
Custom Contrôle total Plus de travail
Voir le code
%%writefile /tmp/spark-docker/Dockerfile.basic
# Image Spark de base
FROM bitnami/spark:3.5

# Passer en root pour installer des packages
USER root

# Installer des dépendances Python
RUN pip install --no-cache-dir \
    boto3 \
    pyarrow \
    pandas

# Copier l'application
COPY app/ /app/

# Revenir à l'utilisateur non-root (sécurité)
USER 1001

# Définir le working directory
WORKDIR /app

3.2 Image production-grade avec JARs

Voir le code
%%writefile /tmp/spark-docker/Dockerfile.production
# ============================================
# Spark Production Image
# ============================================
FROM bitnami/spark:3.5 AS base

USER root

# ---- Dépendances système ----
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# ---- Dépendances Python ----
COPY requirements.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/requirements.txt

# ---- JARs pour connecteurs cloud ----
# AWS S3
RUN curl -sL https://repo1.maven.org/maven2/org/apache/hadoop/hadoop-aws/3.3.4/hadoop-aws-3.3.4.jar \
    -o /opt/bitnami/spark/jars/hadoop-aws-3.3.4.jar
RUN curl -sL https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-bundle/1.12.262/aws-java-sdk-bundle-1.12.262.jar \
    -o /opt/bitnami/spark/jars/aws-java-sdk-bundle-1.12.262.jar

# ---- Application ----
COPY app/ /app/

# ---- Sécurité : non-root ----
RUN chown -R 1001:1001 /app
USER 1001

WORKDIR /app

# ---- Healthcheck ----
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:4040/api/v1/applications || exit 1
Voir le code
%%writefile /tmp/spark-docker/requirements.txt
# Python dependencies for Spark jobs
boto3>=1.28.0
pyarrow>=14.0.0
pandas>=2.0.0
requests>=2.31.0

3.3 Best practices Docker pour Spark

Pratique Pourquoi Comment
Non-root Sécurité USER 1001
Multi-stage builds Image plus légère FROM ... AS builder
Layer caching Builds plus rapides Dépendances avant code
No cache pip Image plus légère --no-cache-dir
Healthcheck K8s liveness probe HEALTHCHECK
Versioning Reproductibilité Tags explicites (pas latest)
Voir le code
# Commandes pour construire et pousser l'image
docker_commands = """
# Construire l'image
docker build -t my-registry/spark-app:1.0.0 -f Dockerfile.production .

# Tester localement
docker run --rm my-registry/spark-app:1.0.0 spark-submit --version

# Pousser vers un registry
docker push my-registry/spark-app:1.0.0

# Pour Minikube (utiliser le Docker daemon de Minikube)
eval $(minikube docker-env)
docker build -t spark-app:1.0.0 .
"""
print(docker_commands)

Exercice 2 : Construire une image Spark

Objectif : Créer un Dockerfile Spark avec :

  • Base bitnami/spark:3.5
  • Dépendance Python : numpy
  • Un script hello.py qui affiche “Hello Spark on K8s!”
💡 Solution
FROM bitnami/spark:3.5

USER root
RUN pip install --no-cache-dir numpy

COPY hello.py /app/hello.py

USER 1001
WORKDIR /app
# hello.py
from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("Hello").getOrCreate()
print("Hello Spark on K8s!")
spark.stop()

4. Configuration Kubernetes pour Spark

4.1 Namespace dédié

Voir le code
%%writefile /tmp/spark-k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: spark
  labels:
    app: spark
    environment: development

4.2 ServiceAccount & RBAC

Spark a besoin de permissions pour :

  • Créer des Pods (executors)
  • Lister/supprimer des Pods
  • Accéder aux ConfigMaps et Secrets
Voir le code
%%writefile /tmp/spark-k8s/rbac.yaml
# ServiceAccount pour Spark
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spark-sa
  namespace: spark
---
# Role avec permissions minimales
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: spark-role
  namespace: spark
rules:
  # Gérer les pods (executors)
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create", "get", "list", "watch", "delete"]
  # Logs des pods
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get", "list"]
  # ConfigMaps pour Spark config
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["create", "get", "list", "watch", "delete"]
  # Services pour Spark UI
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create", "get", "delete"]
  # PersistentVolumeClaims
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["create", "get", "list", "delete"]
---
# Binding Role → ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: spark-role-binding
  namespace: spark
subjects:
  - kind: ServiceAccount
    name: spark-sa
    namespace: spark
roleRef:
  kind: Role
  name: spark-role
  apiGroup: rbac.authorization.k8s.io

4.3 Secrets (credentials cloud)

⚠️ Note : En production, utilise un gestionnaire de secrets (Vault, AWS Secrets Manager, etc.)

Voir le code
%%writefile /tmp/spark-k8s/secrets.yaml
# Secret pour MinIO (ou S3)
apiVersion: v1
kind: Secret
metadata:
  name: minio-credentials
  namespace: spark
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "minioadmin"
  AWS_SECRET_ACCESS_KEY: "minioadmin"
  S3_ENDPOINT: "http://minio.minio.svc.cluster.local:9000"
Voir le code
# Alternative : créer le secret via CLI
secret_command = """
kubectl create secret generic minio-credentials \
  --namespace=spark \
  --from-literal=AWS_ACCESS_KEY_ID=minioadmin \
  --from-literal=AWS_SECRET_ACCESS_KEY=minioadmin \
  --from-literal=S3_ENDPOINT=http://minio.minio.svc.cluster.local:9000
"""
print(secret_command)

4.4 PersistentVolumeClaims

Voir le code
%%writefile /tmp/spark-k8s/pvc.yaml
# PVC pour les logs Spark
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: spark-logs-pvc
  namespace: spark
spec:
  accessModes:
    - ReadWriteMany  # Plusieurs pods peuvent écrire
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard  # Adapter selon ton cluster
---
# PVC pour les checkpoints (streaming)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: spark-checkpoints-pvc
  namespace: spark
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 20Gi
  storageClassName: standard

4.5 ResourceQuotas & LimitRanges (optionnel mais recommandé)

Voir le code
%%writefile /tmp/spark-k8s/quotas.yaml
# Limiter les ressources du namespace Spark
apiVersion: v1
kind: ResourceQuota
metadata:
  name: spark-quota
  namespace: spark
spec:
  hard:
    requests.cpu: "20"        # Max 20 CPU demandés
    requests.memory: "40Gi"   # Max 40 Gi mémoire demandée
    limits.cpu: "40"          # Max 40 CPU limites
    limits.memory: "80Gi"     # Max 80 Gi mémoire limites
    pods: "50"                # Max 50 pods
---
# Defaults pour les pods sans spec
apiVersion: v1
kind: LimitRange
metadata:
  name: spark-limits
  namespace: spark
spec:
  limits:
    - type: Container
      default:
        cpu: "1"
        memory: "2Gi"
      defaultRequest:
        cpu: "500m"
        memory: "1Gi"
      max:
        cpu: "8"
        memory: "16Gi"

Exercice 3 : Créer les manifests RBAC

Question : Pourquoi le ServiceAccount Spark a-t-il besoin de la permission pods/log ?

💡 Réponse

Le Driver Spark a besoin de lire les logs des Executor Pods pour :

  • Afficher les erreurs dans le Spark UI
  • Permettre le debugging via kubectl logs
  • Collecter les métriques d’exécution

5. spark-submit sur Kubernetes

5.1 Syntaxe de base

Voir le code
spark_submit_basic = """
# Spark-submit basique sur K8s (cluster mode)
spark-submit \
  --master k8s://https://<kubernetes-api-server>:6443 \
  --deploy-mode cluster \
  --name spark-pi \
  --conf spark.kubernetes.container.image=spark-app:1.0.0 \
  --conf spark.kubernetes.namespace=spark \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark-sa \
  local:///app/pi.py
"""
print(spark_submit_basic)

5.2 Commande complète production-grade

Voir le code
spark_submit_full = """
# Spark-submit complet pour production
spark-submit \
  # === Master & Mode ===
  --master k8s://https://$(kubectl config view -o jsonpath='{.clusters[0].cluster.server}') \
  --deploy-mode cluster \
  --name etl-job-$(date +%Y%m%d-%H%M%S) \
  \
  # === Image & Namespace ===
  --conf spark.kubernetes.container.image=my-registry/spark-app:1.0.0 \
  --conf spark.kubernetes.namespace=spark \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark-sa \
  \
  # === Driver Resources ===
  --conf spark.driver.cores=2 \
  --conf spark.driver.memory=4g \
  --conf spark.kubernetes.driver.request.cores=1 \
  --conf spark.kubernetes.driver.limit.cores=2 \
  \
  # === Executor Resources ===
  --conf spark.executor.instances=4 \
  --conf spark.executor.cores=2 \
  --conf spark.executor.memory=4g \
  --conf spark.kubernetes.executor.request.cores=1 \
  --conf spark.kubernetes.executor.limit.cores=2 \
  \
  # === Secrets (environnement) ===
  --conf spark.kubernetes.driver.secretKeyRef.AWS_ACCESS_KEY_ID=minio-credentials:AWS_ACCESS_KEY_ID \
  --conf spark.kubernetes.driver.secretKeyRef.AWS_SECRET_ACCESS_KEY=minio-credentials:AWS_SECRET_ACCESS_KEY \
  --conf spark.kubernetes.executor.secretKeyRef.AWS_ACCESS_KEY_ID=minio-credentials:AWS_ACCESS_KEY_ID \
  --conf spark.kubernetes.executor.secretKeyRef.AWS_SECRET_ACCESS_KEY=minio-credentials:AWS_SECRET_ACCESS_KEY \
  \
  # === S3/MinIO Config ===
  --conf spark.hadoop.fs.s3a.endpoint=http://minio.minio.svc.cluster.local:9000 \
  --conf spark.hadoop.fs.s3a.path.style.access=true \
  --conf spark.hadoop.fs.s3a.impl=org.apache.hadoop.fs.s3a.S3AFileSystem \
  \
  # === Application ===
  local:///app/etl_job.py \
  --input s3a://bronze/data \
  --output s3a://silver/data
"""
print(spark_submit_full)

5.3 Configurations essentielles

Configuration Description Exemple
spark.kubernetes.container.image Image Docker Spark registry/spark:1.0
spark.kubernetes.namespace Namespace K8s spark
spark.kubernetes.authenticate.driver.serviceAccountName ServiceAccount spark-sa
spark.executor.instances Nombre d’executors 4
spark.executor.cores Cores par executor 2
spark.executor.memory Mémoire par executor 4g
spark.kubernetes.driver.secretKeyRef.* Secrets → env vars Voir exemple
spark.kubernetes.executor.deleteOnTermination Cleanup true

5.4 Accéder au Spark UI

En cluster mode, le Spark UI est dans le Pod Driver.

Voir le code
spark_ui_access = """
# 1. Trouver le pod Driver
kubectl get pods -n spark -l spark-role=driver

# 2. Port-forward vers le Spark UI
kubectl port-forward -n spark <driver-pod-name> 4040:4040

# 3. Ouvrir dans le navigateur
# http://localhost:4040

# Alternative : créer un Service
kubectl expose pod <driver-pod-name> -n spark \
  --port=4040 --target-port=4040 \
  --name=spark-ui --type=NodePort
"""
print(spark_ui_access)

Exercice 4 : Lancer un job Spark sur Minikube

Étapes : 1. Démarrer Minikube : minikube start --cpus=4 --memory=8g 2. Créer le namespace et RBAC 3. Lancer le job SparkPi intégré

💡 Solution
# 1. Setup
minikube start --cpus=4 --memory=8g
kubectl create namespace spark
kubectl apply -f rbac.yaml

# 2. Obtenir l'URL de l'API K8s
K8S_API=$(kubectl config view -o jsonpath='{.clusters[0].cluster.server}')

# 3. Lancer SparkPi
spark-submit \
  --master k8s://$K8S_API \
  --deploy-mode cluster \
  --name spark-pi \
  --conf spark.kubernetes.container.image=apache/spark:3.5.0 \
  --conf spark.kubernetes.namespace=spark \
  --conf spark.kubernetes.authenticate.driver.serviceAccountName=spark-sa \
  --conf spark.executor.instances=2 \
  local:///opt/spark/examples/src/main/python/pi.py 100

# 4. Vérifier
kubectl get pods -n spark
kubectl logs -n spark <driver-pod> | tail -20

6. Spark Operator — Production-Grade

6.1 Pourquoi utiliser Spark Operator ?

spark-submit Spark Operator
Commande CLI Manifeste YAML déclaratif
Pas de retry automatique Retry policy configurable
Pas de scheduling CronJob-like scheduling
Difficile à intégrer CI/CD GitOps natif
Logs dispersés Logs centralisés

6.2 Installation avec Helm

Voir le code
operator_install = """
# Ajouter le repo Helm
helm repo add spark-operator https://kubeflow.github.io/spark-operator
helm repo update

# Installer l'opérateur
helm install spark-operator spark-operator/spark-operator \
  --namespace spark-operator \
  --create-namespace \
  --set webhook.enable=true \
  --set sparkJobNamespace=spark

# Vérifier
kubectl get pods -n spark-operator
"""
print(operator_install)

6.3 SparkApplication CRD

Voir le code
%%writefile /tmp/spark-k8s/spark-application.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: etl-job
  namespace: spark
spec:
  type: Python
  pythonVersion: "3"
  mode: cluster
  image: my-registry/spark-app:1.0.0
  imagePullPolicy: Always
  mainApplicationFile: local:///app/etl_job.py
  arguments:
    - "--input"
    - "s3a://bronze/data"
    - "--output"
    - "s3a://silver/data"
  sparkVersion: "3.5.0"
  
  # Configuration Spark
  sparkConf:
    "spark.hadoop.fs.s3a.endpoint": "http://minio.minio.svc.cluster.local:9000"
    "spark.hadoop.fs.s3a.path.style.access": "true"
    "spark.hadoop.fs.s3a.impl": "org.apache.hadoop.fs.s3a.S3AFileSystem"
  
  # Restart policy
  restartPolicy:
    type: OnFailure
    onFailureRetries: 3
    onFailureRetryInterval: 30
    onSubmissionFailureRetries: 3
  
  # Driver config
  driver:
    cores: 1
    coreLimit: "1200m"
    memory: "2g"
    serviceAccount: spark-sa
    labels:
      app: spark-etl
      role: driver
    envSecretKeyRefs:
      AWS_ACCESS_KEY_ID:
        name: minio-credentials
        key: AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY:
        name: minio-credentials
        key: AWS_SECRET_ACCESS_KEY
  
  # Executor config
  executor:
    cores: 2
    coreLimit: "2400m"
    instances: 4
    memory: "4g"
    labels:
      app: spark-etl
      role: executor
    envSecretKeyRefs:
      AWS_ACCESS_KEY_ID:
        name: minio-credentials
        key: AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY:
        name: minio-credentials
        key: AWS_SECRET_ACCESS_KEY

6.4 ScheduledSparkApplication (Cron jobs)

Voir le code
%%writefile /tmp/spark-k8s/scheduled-spark.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: ScheduledSparkApplication
metadata:
  name: daily-etl
  namespace: spark
spec:
  schedule: "0 2 * * *"  # Tous les jours à 2h du matin
  concurrencyPolicy: Forbid  # Ne pas lancer si le précédent tourne encore
  successfulRunHistoryLimit: 5
  failedRunHistoryLimit: 3
  
  template:
    type: Python
    pythonVersion: "3"
    mode: cluster
    image: my-registry/spark-app:1.0.0
    mainApplicationFile: local:///app/daily_etl.py
    sparkVersion: "3.5.0"
    
    restartPolicy:
      type: OnFailure
      onFailureRetries: 2
    
    driver:
      cores: 1
      memory: "2g"
      serviceAccount: spark-sa
    
    executor:
      cores: 2
      instances: 3
      memory: "4g"
Voir le code
operator_commands = """
# Appliquer une SparkApplication
kubectl apply -f spark-application.yaml

# Voir le statut
kubectl get sparkapplication -n spark
kubectl describe sparkapplication etl-job -n spark

# Voir les logs du driver
kubectl logs -n spark -l spark-role=driver -f

# Supprimer
kubectl delete sparkapplication etl-job -n spark
"""
print(operator_commands)

Exercice 5 : Déployer avec SparkOperator

Objectif : Créer une SparkApplication qui calcule Pi avec 1000 itérations.

💡 Solution
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: spark-pi
  namespace: spark
spec:
  type: Python
  pythonVersion: "3"
  mode: cluster
  image: apache/spark:3.5.0
  mainApplicationFile: local:///opt/spark/examples/src/main/python/pi.py
  arguments: ["1000"]
  sparkVersion: "3.5.0"
  
  restartPolicy:
    type: Never
  
  driver:
    cores: 1
    memory: "1g"
    serviceAccount: spark-sa
  
  executor:
    cores: 1
    instances: 2
    memory: "1g"

7. Autoscaling & Resource Management

7.1 Dynamic Allocation (Spark natif)

Spark peut ajuster dynamiquement le nombre d’executors.

Voir le code
dynamic_allocation_config = """
# Dans sparkConf ou spark-submit
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=1
spark.dynamicAllocation.maxExecutors=10
spark.dynamicAllocation.initialExecutors=2
spark.dynamicAllocation.executorIdleTimeout=60s
spark.dynamicAllocation.schedulerBacklogTimeout=1s

# Shuffle tracking (requis pour K8s)
spark.dynamicAllocation.shuffleTracking.enabled=true
spark.dynamicAllocation.shuffleTracking.timeout=600s
"""
print(dynamic_allocation_config)

7.2 Options d’autoscaling K8s

Type Description Use case
Dynamic Allocation Spark gère les executors ✅ Recommandé pour batch
HPA Scale sur CPU/memory ⚠️ Pas idéal pour Spark
VPA Ajuste les ressources 💡 Pour dimensionnement initial
KEDA Scale sur événements externes ✅ Idéal pour streaming (Kafka lag)
Cluster Autoscaler Ajoute/retire des nodes ✅ Combiné avec Dynamic Allocation
Voir le code
%%writefile /tmp/spark-k8s/keda-scaledobject.yaml
# KEDA ScaledObject pour Spark Streaming basé sur Kafka lag
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: spark-streaming-scaler
  namespace: spark
spec:
  scaleTargetRef:
    name: spark-streaming  # Deployment à scaler
  minReplicaCount: 1
  maxReplicaCount: 10
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: kafka.kafka.svc.cluster.local:9092
        consumerGroup: spark-streaming-group
        topic: events
        lagThreshold: "100"  # Scale si lag > 100

7.3 Node Affinity & Tolerations

Voir le code
%%writefile /tmp/spark-k8s/spark-with-affinity.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: gpu-job
  namespace: spark
spec:
  type: Python
  mode: cluster
  image: my-registry/spark-gpu:1.0.0
  mainApplicationFile: local:///app/gpu_job.py
  sparkVersion: "3.5.0"
  
  driver:
    cores: 1
    memory: "2g"
    serviceAccount: spark-sa
  
  executor:
    cores: 4
    instances: 2
    memory: "8g"
    # Placer les executors sur des nodes avec GPU
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
            - matchExpressions:
                - key: node-type
                  operator: In
                  values:
                    - gpu
    # Tolérer les taints GPU
    tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"

Exercice 6 : Configurer Dynamic Allocation

Objectif : Modifier la SparkApplication pour avoir :

  • Min 2 executors
  • Max 8 executors
  • Initial 3 executors
💡 Solution
sparkConf:
  "spark.dynamicAllocation.enabled": "true"
  "spark.dynamicAllocation.minExecutors": "2"
  "spark.dynamicAllocation.maxExecutors": "8"
  "spark.dynamicAllocation.initialExecutors": "3"
  "spark.dynamicAllocation.shuffleTracking.enabled": "true"

8. Monitoring & Observability

8.1 Spark UI

Voir le code
%%writefile /tmp/spark-k8s/spark-ui-ingress.yaml
# Service pour Spark UI
apiVersion: v1
kind: Service
metadata:
  name: spark-ui
  namespace: spark
spec:
  selector:
    spark-role: driver
  ports:
    - port: 4040
      targetPort: 4040
  type: ClusterIP
---
# Ingress pour accès externe
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: spark-ui-ingress
  namespace: spark
spec:
  rules:
    - host: spark-ui.mycompany.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: spark-ui
                port:
                  number: 4040

8.2 Spark History Server

Voir le code
%%writefile /tmp/spark-k8s/history-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spark-history-server
  namespace: spark
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spark-history-server
  template:
    metadata:
      labels:
        app: spark-history-server
    spec:
      containers:
        - name: history-server
          image: bitnami/spark:3.5
          command:
            - /opt/bitnami/spark/sbin/start-history-server.sh
          env:
            - name: SPARK_HISTORY_OPTS
              value: "-Dspark.history.fs.logDirectory=s3a://spark-logs/history"
          ports:
            - containerPort: 18080
          envFrom:
            - secretRef:
                name: minio-credentials
---
apiVersion: v1
kind: Service
metadata:
  name: spark-history-server
  namespace: spark
spec:
  selector:
    app: spark-history-server
  ports:
    - port: 18080
      targetPort: 18080
  type: ClusterIP

Exercice 7 : Exposer Spark UI

Objectif : Accéder au Spark UI d’un job en cours.

# 1. Lister les pods driver
kubectl get pods -n spark -l spark-role=driver

# 2. Port-forward
kubectl port-forward -n spark <driver-pod> 4040:4040

# 3. Ouvrir http://localhost:4040

9. Debugging & Troubleshooting

9.1 Erreurs courantes et solutions

Erreur Cause Solution
ImagePullBackOff Image introuvable Vérifier registry, credentials, tag
OOMKilled Mémoire insuffisante Augmenter memory, réduire données
Forbidden RBAC manquant Vérifier Role/RoleBinding
Pending (pods) Ressources insuffisantes Vérifier quotas, cluster capacity
Connection refused Driver inaccessible Vérifier network policies, services
ClassNotFoundException JAR manquant Ajouter dans l’image Docker
Voir le code
debug_commands = """
# === Debugging Spark on K8s ===

# 1. Voir les pods et leur statut
kubectl get pods -n spark -o wide

# 2. Détails d'un pod (événements)
kubectl describe pod <pod-name> -n spark

# 3. Logs du driver
kubectl logs -n spark -l spark-role=driver

# 4. Logs d'un executor spécifique
kubectl logs -n spark <executor-pod-name>

# 5. Logs en temps réel (follow)
kubectl logs -n spark -l spark-role=driver -f

# 6. Shell dans un pod (debug)
kubectl exec -it <pod-name> -n spark -- /bin/bash

# 7. Vérifier les ressources du namespace
kubectl describe resourcequota -n spark

# 8. Vérifier les events
kubectl get events -n spark --sort-by='.lastTimestamp'

# 9. SparkApplication status
kubectl get sparkapplication -n spark
kubectl describe sparkapplication <name> -n spark
"""
print(debug_commands)

9.2 Debugging OOMKilled

🔍 Symptôme : Pod executor en état OOMKilled

Causes possibles :
├── spark.executor.memory trop bas
├── spark.executor.memoryOverhead mal configuré
├── Trop de données par partition
├── Broadcast variables trop grandes
└── Fuite mémoire dans le code

Solutions :
├── Augmenter spark.executor.memory
├── spark.executor.memoryOverhead = max(0.1 × memory, 384m)
├── Repartitionner : df.repartition(200)
├── Réduire spark.sql.shuffle.partitions
└── Utiliser spark.memory.fraction = 0.6 (default)

9.3 Debugging Shuffle failures

Le shuffle est plus coûteux sur K8s (pas de data locality).

Voir le code
shuffle_optimizations = """
# Optimisations shuffle pour K8s

# Augmenter les timeouts
spark.network.timeout=600s
spark.shuffle.io.maxRetries=10
spark.shuffle.io.retryWait=30s

# Compression
spark.shuffle.compress=true
spark.shuffle.spill.compress=true

# Buffer sizes
spark.shuffle.file.buffer=64k
spark.reducer.maxSizeInFlight=96m

# Shuffle tracking (pour Dynamic Allocation)
spark.dynamicAllocation.shuffleTracking.enabled=true
"""
print(shuffle_optimizations)

Exercice 8 : Diagnostiquer un job qui échoue

Scénario : Un job Spark échoue avec le message “Pod OOMKilled”.

Questions :

  1. Quelle commande pour voir les events du pod ?
  2. Quelles configs Spark vérifier ?
💡 Solution
  1. Commandes :
kubectl describe pod <pod-name> -n spark
kubectl get events -n spark --field-selector involvedObject.name=<pod-name>
  1. Configs à vérifier :
spark.executor.memory
spark.executor.memoryOverhead
spark.kubernetes.executor.limit.memory

10. Optimisations Spark on K8s

10.1 Data Locality

⚠️ Problème : Pas de data locality sur K8s

Sur YARN avec HDFS :
┌──────────────────────┐
│ Node 1               │
│ ┌────────┐ ┌───────┐ │
│ │Executor│◀│ Data  │ │  ← Data local (fast)
│ └────────┘ └───────┘ │
└──────────────────────┘

Sur K8s avec Object Storage :
┌──────────────────────┐      ┌─────────────┐
│ Node 1               │      │ S3/MinIO    │
│ ┌────────┐           │◀─────│ (remote)    │  ← Network transfer
│ │Executor│           │      │             │
│ └────────┘           │      └─────────────┘
└──────────────────────┘

Solutions :

  • Utiliser des formats colonnaires (Parquet) optimisés
  • Configurer S3A/GCS pour le parallélisme
  • External Shuffle Service (Spark 3.4+)
Voir le code
s3a_config = """
# Optimisations S3A pour Spark on K8s

# Parallélisme
spark.hadoop.fs.s3a.connection.maximum=200
spark.hadoop.fs.s3a.threads.max=64
spark.hadoop.fs.s3a.threads.core=16

# Fast upload
spark.hadoop.fs.s3a.fast.upload=true
spark.hadoop.fs.s3a.fast.upload.buffer=bytebuffer
spark.hadoop.fs.s3a.multipart.size=104857600  # 100MB

# Committer (éviter les renames S3)
spark.sql.sources.commitProtocolClass=org.apache.spark.internal.io.cloud.PathOutputCommitProtocol
spark.sql.parquet.output.committer.class=org.apache.spark.internal.io.cloud.BindingParquetOutputCommitter
"""
print(s3a_config)

10.2 Pod Templates

Pour des configurations avancées des pods Spark.

Voir le code
%%writefile /tmp/spark-k8s/executor-pod-template.yaml
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: spark-executor
      volumeMounts:
        - name: spark-local-dir
          mountPath: /tmp/spark
      resources:
        requests:
          ephemeral-storage: "10Gi"
        limits:
          ephemeral-storage: "20Gi"
  volumes:
    - name: spark-local-dir
      emptyDir:
        medium: Memory  # Utiliser RAM pour shuffle local
        sizeLimit: "4Gi"
Voir le code
pod_template_usage = """
# Utiliser le pod template dans spark-submit
spark-submit \
  --conf spark.kubernetes.executor.podTemplateFile=/path/to/executor-pod-template.yaml \
  ...

# Ou dans SparkApplication
spec:
  executor:
    podTemplateFile: /path/to/executor-pod-template.yaml
"""
print(pod_template_usage)

10.3 Spot/Preemptible Instances

Économiser jusqu’à 90% sur les coûts compute.

Voir le code
%%writefile /tmp/spark-k8s/spark-with-spot.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: spot-etl
  namespace: spark
spec:
  type: Python
  mode: cluster
  image: my-registry/spark-app:1.0.0
  mainApplicationFile: local:///app/etl.py
  sparkVersion: "3.5.0"
  
  # Driver sur nodes stables (on-demand)
  driver:
    cores: 1
    memory: "2g"
    serviceAccount: spark-sa
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
            - matchExpressions:
                - key: node-type
                  operator: In
                  values:
                    - on-demand
  
  # Executors sur spot instances
  executor:
    cores: 2
    instances: 5
    memory: "4g"
    affinity:
      nodeAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            preference:
              matchExpressions:
                - key: node-type
                  operator: In
                  values:
                    - spot
    tolerations:
      - key: "kubernetes.azure.com/scalesetpriority"
        operator: "Equal"
        value: "spot"
        effect: "NoSchedule"

11. Mini-Projet : ETL Pipeline sur Kubernetes

Objectif

Déployer un pipeline Spark complet sur K8s avec MinIO (Object Storage local).

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     Kubernetes Cluster                          │
│  ┌─────────────┐     ┌──────────────────┐     ┌─────────────┐  │
│  │   MinIO     │     │      Spark       │     │   MinIO     │  │
│  │  (source)   │────▶│    on K8s        │────▶│  (output)   │  │
│  │             │     │                  │     │             │  │
│  │ bucket:     │     │  ┌────────────┐  │     │ bucket:     │  │
│  │ bronze/     │     │  │  Driver    │  │     │ silver/     │  │
│  │   data.csv  │     │  └────────────┘  │     │   data.parquet│  │
│  │             │     │  ┌────┐ ┌────┐   │     │             │  │
│  │             │     │  │Exec│ │Exec│   │     │             │  │
│  │             │     │  └────┘ └────┘   │     │             │  │
│  └─────────────┘     └──────────────────┘     └─────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Structure du projet

spark-k8s-project/
├── docker/
│   ├── Dockerfile
│   └── requirements.txt
├── app/
│   └── etl_job.py
├── manifests/
│   ├── namespace.yaml
│   ├── rbac.yaml
│   ├── minio-secret.yaml
│   └── spark-application.yaml
├── data/
│   └── sample.csv
└── README.md

Étape 1 : Déployer MinIO sur K8s

Voir le code
minio_deploy = """
# Déployer MinIO avec Helm
helm repo add minio https://charts.min.io/
helm repo update

helm install minio minio/minio \
  --namespace minio \
  --create-namespace \
  --set rootUser=minioadmin \
  --set rootPassword=minioadmin \
  --set mode=standalone \
  --set resources.requests.memory=512Mi \
  --set persistence.size=10Gi

# Port-forward pour accéder à la console
kubectl port-forward -n minio svc/minio-console 9001:9001

# Créer les buckets
mc alias set myminio http://localhost:9000 minioadmin minioadmin
mc mb myminio/bronze
mc mb myminio/silver

# Uploader des données de test
mc cp data/sample.csv myminio/bronze/
"""
print(minio_deploy)

Étape 2 : Code de l’application ETL

Voir le code
%%writefile /tmp/spark-k8s-project/app/etl_job.py
"""
ETL Job : Bronze → Silver
Lit des CSV depuis MinIO, transforme, et écrit en Parquet.
"""
import argparse
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, upper, current_timestamp


def create_spark_session():
    """Créer une SparkSession configurée pour MinIO."""
    return SparkSession.builder \
        .appName("ETL Bronze to Silver") \
        .config("spark.hadoop.fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem") \
        .config("spark.hadoop.fs.s3a.path.style.access", "true") \
        .getOrCreate()


def extract(spark, input_path):
    """Lire les données brutes depuis le bucket Bronze."""
    print(f"📥 Reading from {input_path}")
    df = spark.read \
        .option("header", "true") \
        .option("inferSchema", "true") \
        .csv(input_path)
    print(f"   Loaded {df.count()} rows")
    return df


def transform(df):
    """Appliquer les transformations métier."""
    print("🔄 Transforming data")
    transformed = df \
        .dropDuplicates() \
        .dropna() \
        .withColumn("processed_at", current_timestamp())
    
    # Normalisation des colonnes string
    for col_name, dtype in transformed.dtypes:
        if dtype == "string":
            transformed = transformed.withColumn(col_name, upper(col(col_name)))
    
    print(f"   Transformed to {transformed.count()} rows")
    return transformed


def load(df, output_path):
    """Écrire les données transformées dans le bucket Silver."""
    print(f"📤 Writing to {output_path}")
    df.write \
        .mode("overwrite") \
        .parquet(output_path)
    print("   Done!")


def main():
    parser = argparse.ArgumentParser(description="ETL Job")
    parser.add_argument("--input", required=True, help="Input path (s3a://...)")
    parser.add_argument("--output", required=True, help="Output path (s3a://...)")
    args = parser.parse_args()
    
    spark = create_spark_session()
    
    try:
        # ETL Pipeline
        df = extract(spark, args.input)
        transformed = transform(df)
        load(transformed, args.output)
        
        print("✅ ETL completed successfully!")
    except Exception as e:
        print(f"❌ ETL failed: {e}")
        raise
    finally:
        spark.stop()


if __name__ == "__main__":
    main()

Étape 3 : Dockerfile

Voir le code
%%writefile /tmp/spark-k8s-project/docker/Dockerfile
FROM bitnami/spark:3.5

USER root

# Dépendances Python
RUN pip install --no-cache-dir pyarrow pandas

# JARs pour S3/MinIO
RUN curl -sL https://repo1.maven.org/maven2/org/apache/hadoop/hadoop-aws/3.3.4/hadoop-aws-3.3.4.jar \
    -o /opt/bitnami/spark/jars/hadoop-aws-3.3.4.jar && \
    curl -sL https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-bundle/1.12.262/aws-java-sdk-bundle-1.12.262.jar \
    -o /opt/bitnami/spark/jars/aws-java-sdk-bundle-1.12.262.jar

# Application
COPY app/ /app/
RUN chown -R 1001:1001 /app

USER 1001
WORKDIR /app

Étape 4 : Manifests Kubernetes

Voir le code
%%writefile /tmp/spark-k8s-project/manifests/spark-application.yaml
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
  name: etl-bronze-silver
  namespace: spark
spec:
  type: Python
  pythonVersion: "3"
  mode: cluster
  image: spark-etl:1.0.0  # Image locale (Minikube)
  imagePullPolicy: Never  # Pour Minikube
  mainApplicationFile: local:///app/etl_job.py
  arguments:
    - "--input"
    - "s3a://bronze/sample.csv"
    - "--output"
    - "s3a://silver/data"
  sparkVersion: "3.5.0"
  
  sparkConf:
    "spark.hadoop.fs.s3a.endpoint": "http://minio.minio.svc.cluster.local:9000"
    "spark.hadoop.fs.s3a.path.style.access": "true"
    "spark.hadoop.fs.s3a.impl": "org.apache.hadoop.fs.s3a.S3AFileSystem"
  
  restartPolicy:
    type: OnFailure
    onFailureRetries: 2
  
  driver:
    cores: 1
    memory: "1g"
    serviceAccount: spark-sa
    envSecretKeyRefs:
      AWS_ACCESS_KEY_ID:
        name: minio-credentials
        key: AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY:
        name: minio-credentials
        key: AWS_SECRET_ACCESS_KEY
  
  executor:
    cores: 1
    instances: 2
    memory: "1g"
    envSecretKeyRefs:
      AWS_ACCESS_KEY_ID:
        name: minio-credentials
        key: AWS_ACCESS_KEY_ID
      AWS_SECRET_ACCESS_KEY:
        name: minio-credentials
        key: AWS_SECRET_ACCESS_KEY

Étape 5 : Déploiement complet

Voir le code
deployment_script = """
#!/bin/bash
set -e

echo "🚀 Déploiement du pipeline ETL Spark on K8s"

# 1. Créer le namespace et RBAC
echo "📁 Création namespace et RBAC..."
kubectl apply -f manifests/namespace.yaml
kubectl apply -f manifests/rbac.yaml
kubectl apply -f manifests/minio-secret.yaml

# 2. Build de l'image (Minikube)
echo "🐳 Build de l'image Docker..."
eval $(minikube docker-env)
docker build -t spark-etl:1.0.0 -f docker/Dockerfile .

# 3. Vérifier que MinIO est prêt
echo "⏳ Attente de MinIO..."
kubectl wait --for=condition=ready pod -l app=minio -n minio --timeout=120s

# 4. Lancer le job Spark
echo "🔥 Lancement du job Spark..."
kubectl apply -f manifests/spark-application.yaml

# 5. Suivre le job
echo "📊 Suivi du job..."
kubectl get sparkapplication -n spark -w
"""
print(deployment_script)

Étape 6 : Vérification

Voir le code
verify_commands = """
# Vérifier le statut du job
kubectl get sparkapplication -n spark

# Voir les logs du driver
kubectl logs -n spark -l spark-role=driver

# Vérifier les outputs dans MinIO
mc ls myminio/silver/data/

# Lire un sample des données
mc cat myminio/silver/data/part-00000.parquet | head
"""
print(verify_commands)

🧪 Quiz de fin de module


❓ Q1. Quelle est la différence entre Client mode et Cluster mode ?

  1. Client mode est plus rapide
  2. En Cluster mode, le Driver tourne dans un Pod K8s
  3. Client mode ne supporte pas les executors
  4. Cluster mode nécessite YARN
💡 Voir la réponse

Réponse : b — En Cluster mode, le Driver est un Pod K8s. En Client mode, il reste sur la machine locale.


❓ Q2. Pourquoi utiliser Spark Operator plutôt que spark-submit directement ?

  1. Spark Operator est plus rapide
  2. spark-submit ne fonctionne pas sur K8s
  3. Spark Operator permet des manifestes YAML déclaratifs et des retries automatiques
  4. Spark Operator ne nécessite pas de ServiceAccount
💡 Voir la réponse

Réponse : c — Spark Operator offre une approche déclarative (YAML), des retry policies, du scheduling, et une meilleure intégration CI/CD.


❓ Q3. Quel problème résout Dynamic Allocation ?

  1. La sécurité des pods
  2. L’ajustement automatique du nombre d’executors
  3. Le stockage des logs
  4. La communication réseau
💡 Voir la réponse

Réponse : b — Dynamic Allocation permet à Spark d’ajuster automatiquement le nombre d’executors selon la charge.


❓ Q4. Quelle config est requise pour Dynamic Allocation sur K8s ?

  1. spark.dynamicAllocation.enabled=true uniquement
  2. spark.dynamicAllocation.shuffleTracking.enabled=true
  3. spark.kubernetes.allocation.batch.size=5
  4. Aucune config spéciale
💡 Voir la réponse

Réponse : b — Sur K8s (sans External Shuffle Service), shuffle tracking est requis pour que Dynamic Allocation fonctionne.


❓ Q5. Que signifie l’erreur OOMKilled ?

  1. Le pod n’a pas d’image
  2. Le pod a dépassé sa limite mémoire
  3. Le réseau est indisponible
  4. Le ServiceAccount est invalide
💡 Voir la réponse

Réponse : b — OOMKilled signifie que le container a dépassé sa limite mémoire et a été tué par K8s.


❓ Q6. Quel est l’avantage principal de K8s sur YARN pour Spark ?

  1. Meilleure data locality
  2. Infrastructure cloud-native et multi-cloud
  3. Plus rapide
  4. Moins de configuration
💡 Voir la réponse

Réponse : b — K8s offre une infrastructure cloud-native, standardisée, multi-cloud, avec autoscaling avancé.


❓ Q7. Comment exposer le Spark UI d’un job en cluster mode ?

  1. Il est automatiquement accessible sur localhost:4040
  2. Via port-forward, Service, ou Ingress
  3. Via YARN ResourceManager
  4. Ce n’est pas possible en cluster mode
💡 Voir la réponse

Réponse : b — On utilise kubectl port-forward, un Service K8s, ou un Ingress pour exposer le Spark UI.


❓ Q8. Pourquoi le shuffle est-il plus coûteux sur K8s que sur YARN/HDFS ?

  1. K8s est plus lent
  2. Pas de data locality — les données doivent transiter par le réseau
  3. Spark n’est pas optimisé pour K8s
  4. Les executors ont moins de mémoire
💡 Voir la réponse

Réponse : b — Sur YARN/HDFS, les données peuvent être locales. Sur K8s avec Object Storage, tout passe par le réseau.


❓ Q9. Quel est le rôle du ServiceAccount dans Spark on K8s ?

  1. Stocker les credentials
  2. Permettre au Driver de créer des Executor Pods
  3. Gérer le Spark UI
  4. Configurer le réseau
💡 Voir la réponse

Réponse : b — Le ServiceAccount donne au Driver les permissions RBAC pour créer, lister et supprimer des Pods (executors).


❓ Q10. Comment économiser sur les coûts compute avec Spark on K8s ?

  1. Utiliser moins d’executors
  2. Désactiver le monitoring
  3. Utiliser des Spot/Preemptible instances pour les executors
  4. Ne pas utiliser de PVC
💡 Voir la réponse

Réponse : c — Les Spot instances peuvent coûter jusqu’à 90% moins cher. Le Driver reste sur des nodes stables, les executors sur Spot.


📚 Ressources pour aller plus loin

🌐 Documentation officielle

📖 Articles & Tutoriels


➡️ Prochaine étape

Maintenant que tu sais déployer Spark sur Kubernetes, passons au Cloud et Object Storage !

👉 Module suivant : 22_cloud_object_storage — Cloud & Object Storage

Tu vas apprendre :

  • Cloud Computing : IaaS, PaaS, SaaS
  • AWS, GCP, Azure : Services Data Engineering
  • Object Storage : S3, GCS, Azure Blob
  • MinIO : Pratiquer localement

📝 Récapitulatif de ce module

Concept Ce que tu as appris
Architecture Driver/Executor Pods, Client vs Cluster mode
Docker Image Build, JARs, best practices
K8s Config RBAC, Secrets, PVC
spark-submit Configurations essentielles
Spark Operator SparkApplication, ScheduledSparkApplication
Autoscaling Dynamic Allocation, KEDA
Monitoring Spark UI, Prometheus, Grafana
Debugging Erreurs courantes, commandes kubectl
Optimisations S3A config, Pod templates, Spot instances

🎉 Félicitations ! Tu as terminé le module Spark on Kubernetes.

Voir le code
# Commandes de nettoyage
cleanup_commands = """
# Supprimer les ressources Spark
kubectl delete sparkapplication --all -n spark
kubectl delete namespace spark

# Supprimer Spark Operator
helm uninstall spark-operator -n spark-operator
kubectl delete namespace spark-operator

# Supprimer MinIO
helm uninstall minio -n minio
kubectl delete namespace minio

# Arrêter Minikube (optionnel)
minikube stop
"""
print(cleanup_commands)
Retour au sommet