Bienvenue dans ce module où tu vas apprendre à déployer et gérer des charges de travail data sur Kubernetes. Tu découvriras les patterns avancés pour les Jobs ETL, les bases de données, le scaling et le monitoring — le tout appliqué au Data Engineering !
Cluster K8s local fonctionnel (Docker Desktop ou Minikube)
Objectifs du module
À la fin de ce module, tu seras capable de :
Comprendre ce qu’est un workload data et ses caractéristiques
Configurer des Jobs et CronJobs avancés pour des ETL robustes
Déployer des bases de données avec StatefulSets
Utiliser Helm pour déployer des stacks data
Gérer le scaling et les ressources pour des workloads gourmands
Mettre en place le monitoring de tes pipelines
Avoir un aperçu de Spark et Airflow sur Kubernetes
C’est quoi un “Workload Data” ?
📊 Un workload data (charge de travail data) désigne toute tâche ou application dédiée au traitement, transformation, ou déplacement de données.
En Data Engineering, les workloads typiques incluent :
Type
Description
Exemple concret
Batch ETL
Traitement planifié de données
Job Python qui transforme des CSV chaque nuit
Ingestion
Chargement de données dans un système
CSV → PostgreSQL, API → Data Lake
Transformation
Calculs et agrégations
Jointures, agrégations, nettoyage
Processing lourd
Calculs intensifs en ressources
Feature engineering, ML preprocessing
Orchestration
Coordination de plusieurs tâches
DAG Airflow avec 10 étapes
Caractéristiques des workloads data
Caractéristique
Explication
Éphémères
S’exécutent puis se terminent (run-to-completion)
Gourmands
Besoin de CPU et RAM significatifs
I/O intensifs
Lecture/écriture de grandes quantités de données
Planifiés
Souvent exécutés selon un schedule (quotidien, horaire)
Reproductibles
Doivent pouvoir être relancés en cas d’échec
Workloads data vs Applications classiques
Aspect
Application web
Workload data
Durée de vie
Continue (24/7)
Éphémère (minutes/heures)
Ressource K8s
Deployment
Job / CronJob
Scaling
Horizontal (replicas)
Vertical (plus de RAM/CPU)
État final
Toujours running
Completed ou Failed
ℹ️ Le savais-tu ?
Le terme “workload” vient du monde des mainframes IBM des années 1960, où il désignait la quantité de travail qu’une machine devait traiter.
Aujourd’hui, dans le contexte cloud-native et Kubernetes, un workload désigne toute unité de travail déployée sur un cluster : une app web, un job batch, un service de streaming, etc.
Les “data workloads” sont simplement les workloads spécialisés dans le traitement de données !
1. Rappels Kubernetes essentiels
Avant d’aller plus loin, voici un récapitulatif rapide des concepts K8s vus dans le module précédent :
Ressource
Rôle
Usage Data Engineering
Pod
Unité de base (1+ containers)
Exécute ton script ETL
Deployment
Gère des replicas de pods
Apps long-running (API, workers)
Job
Tâche one-shot
ETL ponctuel, migration
CronJob
Job planifié
ETL quotidien, rapport hebdo
Service
Expose des pods
Accès à PostgreSQL, APIs
ConfigMap
Config non sensible
Chemins, URLs, paramètres
Secret
Config sensible
Passwords, API keys
PVC
Stockage persistant
Données PostgreSQL, fichiers
Namespace
Isolation logique
Un namespace par projet
💡 Si ces concepts ne sont pas clairs, revois le module 15_kubernetes_fundamentals avant de continuer.
2. Jobs & CronJobs avancés
Les Jobs et CronJobs sont les ressources K8s idéales pour les workloads data.
2.1 Anatomie complète d’un Job (ligne par ligne)
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ JOB : Tâche qui s'exécute jusqu'à complétion ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: batch/v1 # API pour les Jobs et CronJobskind: Job # Type = Job (tâche one-shot)metadata:name: etl-advanced-job # Nom unique du jobnamespace: data-pipeline # Namespace ciblelabels:app: etl # Labels pour filtrer/monitorerteam: data-engineering# ╔══════════════════════════════════════════════════════════════════════════╗# ║ SPEC DU JOB : Paramètres de comportement ║# ╚══════════════════════════════════════════════════════════════════════════╝spec: # ───────────────────────────────────────────────────────────────────────── # COMPLETION ET PARALLÉLISME # ─────────────────────────────────────────────────────────────────────────completions:1 # Nombre de succès requis pour terminer # Ex: 10 = le job doit réussir 10 foisparallelism:1 # Combien de pods tournent EN MÊME TEMPS # Ex: 3 = 3 pods en parallèle # ───────────────────────────────────────────────────────────────────────── # GESTION DES ÉCHECS # ─────────────────────────────────────────────────────────────────────────backoffLimit:3 # Nombre de RETRIES avant échec définitif # Le pod sera relancé 3 fois max si erreuractiveDeadlineSeconds:3600 # TIMEOUT global = 1 heure (3600 secondes) # Si le job dépasse ce temps → échec # ───────────────────────────────────────────────────────────────────────── # NETTOYAGE AUTOMATIQUE # ─────────────────────────────────────────────────────────────────────────ttlSecondsAfterFinished:86400 # Auto-suppression 24h après completion # Sans ça, les vieux jobs s'accumulent ! # ───────────────────────────────────────────────────────────────────────── # TEMPLATE DU POD (ce qui va être exécuté) # ─────────────────────────────────────────────────────────────────────────template:metadata:labels:app: etl # Labels du pod (pour monitoring)spec:containers:-name: etl # Nom du containerimage: my-etl:1.0 # Image Docker # Ressources (TOUJOURS les définir pour les Jobs !)resources:requests: # Minimum garantimemory:"512Mi" # 512 Mo de RAM minimumcpu:"500m" # 0.5 CPU minimumlimits: # Maximum autorisémemory:"1Gi" # 1 Go max (OOMKilled si dépassé)cpu:"1000m" # 1 CPU max (throttled si dépassé)restartPolicy: OnFailure # Que faire si le container crashe ? # OnFailure = relancer le pod # Never = ne pas relancer (backoffLimit gère les retries)
Paramètres clés expliqués
Paramètre
Que fait-il ?
Valeur typique ETL
completions
Combien de succès pour terminer le job ?
1 (une seule exécution)
parallelism
Combien de pods en même temps ?
1 à N selon le use case
backoffLimit
Combien de tentatives en cas d’échec ?
3 à 5
activeDeadlineSeconds
Timeout global du job
1800-7200 (30min-2h)
ttlSecondsAfterFinished
Quand supprimer le job terminé ?
86400 (24h)
restartPolicy
Que faire si crash ?
OnFailure ou Never
2.2 Pattern : Traitement parallèle de fichiers
Imaginons que tu dois traiter 10 fichiers avec 3 pods en parallèle :
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ JOB PARALLÈLE : Chaque pod traite un fichier différent ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: batch/v1kind: Jobmetadata:name: etl-parallelnamespace: data-pipelinespec:completions:10 # 10 fichiers à traiter = 10 succès requisparallelism:3 # 3 pods tournent en même tempscompletionMode: Indexed # ⭐ IMPORTANT : Mode indexé # Chaque pod reçoit un index unique (0, 1, 2, ..., 9) # Permet de savoir quel fichier traiter !template:spec:containers:-name: etlimage: my-etl:1.0command:["python","etl.py"]env: # ⭐ Récupérer l'index du pod (0, 1, 2, ..., 9)-name: FILE_INDEXvalueFrom:fieldRef: # Référence à un champ du pod lui-mêmefieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index'] # Dans ton code Python : # index = os.environ['FILE_INDEX'] # "0", "1", "2", ... # file = f"data_{index}.csv" # data_0.csv, data_1.csv, ...resources:requests:memory:"256Mi"cpu:"200m"limits:memory:"512Mi"cpu:"500m"restartPolicy: OnFailure
┌─────────────────────────────────────────────────────────────────┐
│ FONCTIONNEMENT DU JOB PARALLÈLE INDEXÉ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ completions: 10 parallelism: 3 │
│ ─────────────── ────────────── │
│ │
│ Vague 1 : Pod-0, Pod-1, Pod-2 (en parallèle) │
│ Vague 2 : Pod-3, Pod-4, Pod-5 (quand les précédents finissent)│
│ Vague 3 : Pod-6, Pod-7, Pod-8 │
│ Vague 4 : Pod-9 │
│ │
│ Chaque pod sait quel fichier traiter grâce à FILE_INDEX ! │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 CronJob avancé (ligne par ligne)
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ CRONJOB : Job qui se lance automatiquement selon un schedule ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: batch/v1kind: CronJobmetadata:name: etl-dailynamespace: data-pipelinespec: # ───────────────────────────────────────────────────────────────────────── # SCHEDULE CRON (quand lancer le job ?) # ───────────────────────────────────────────────────────────────────────── # # Format cron : minute heure jour-du-mois mois jour-de-semaine # ┌───────────── minute (0-59) # │ ┌───────────── heure (0-23) # │ │ ┌───────────── jour du mois (1-31) # │ │ │ ┌───────────── mois (1-12) # │ │ │ │ ┌───────────── jour de semaine (0-6, 0=dimanche) # │ │ │ │ │ # * * * * * #schedule:"0 2 * * *" # Tous les jours à 2h00 du matin # ───────────────────────────────────────────────────────────────────────── # POLITIQUE DE CONCURRENCE # ─────────────────────────────────────────────────────────────────────────concurrencyPolicy: Forbid # Que faire si le job précédent tourne encore ? # # Forbid = NE PAS lancer le nouveau (skip) # Allow = Lancer quand même (risque de doublons) # Replace = Tuer l'ancien, lancer le nouveau # # → Pour ETL : Forbid est le plus sûr ! # ───────────────────────────────────────────────────────────────────────── # HISTORIQUE DES JOBS # ─────────────────────────────────────────────────────────────────────────successfulJobsHistoryLimit:3 # Garder les 3 derniers jobs réussisfailedJobsHistoryLimit:2 # Garder les 2 derniers jobs échoués # → Permet de voir les logs des runs précédentes # ───────────────────────────────────────────────────────────────────────── # TOLÉRANCE AU RETARD # ─────────────────────────────────────────────────────────────────────────startingDeadlineSeconds:300 # Si le scheduler a du retard, combien de # secondes de tolérance ? (ici 5 minutes) # Après ce délai, le job est considéré "manqué" # ───────────────────────────────────────────────────────────────────────── # TEMPLATE DU JOB (ce qui est créé à chaque exécution) # ─────────────────────────────────────────────────────────────────────────jobTemplate:spec:backoffLimit:3 # 3 retries si échecactiveDeadlineSeconds:3600 # Timeout 1htemplate:spec:containers:-name: etlimage: my-etl:1.0 # Charger la config depuis un ConfigMapenvFrom:-configMapRef:name: etl-config # Toutes les clés du ConfigMap # deviennent des variables d'environnementresources:requests:memory:"512Mi"cpu:"500m"limits:memory:"1Gi"cpu:"1000m"restartPolicy: OnFailure
Expressions cron courantes
Expression
Signification
Use case
0 2 * * *
Tous les jours à 2h00
ETL quotidien
0 */6 * * *
Toutes les 6 heures
Synchro fréquente
*/15 * * * *
Toutes les 15 minutes
Near real-time
0 0 * * 0
Tous les dimanches à minuit
Rapport hebdo
0 8 1 * *
Le 1er de chaque mois à 8h
Rapport mensuel
0 9-17 * * 1-5
Toutes les heures de 9h à 17h, lun-ven
Heures de bureau
2.4 Commandes utiles pour les Jobs
# Lister les Jobskubectl get jobs -n data-pipeline# Lister les CronJobskubectl get cronjobs -n data-pipeline# Voir les détails d'un CronJobkubectl describe cronjob etl-daily -n data-pipeline# ⭐ Déclencher MANUELLEMENT un CronJob (pour tester)kubectl create job test-etl --from=cronjob/etl-daily -n data-pipeline# Voir les logs du jobkubectl logs job/test-etl -n data-pipeline# Supprimer un jobkubectl delete job test-etl -n data-pipeline
Voir le code
%%bash# Commandes utiles pour les Jobsecho "=== Lister les Jobs ==="kubectl get jobsecho ""echo "=== Lister les CronJobs ==="kubectl get cronjobsecho ""echo "=== Déclencher manuellement un CronJob ==="echo "kubectl create job test-etl --from=cronjob/etl-daily"echo ""echo "=== Voir les logs d'un Job ==="echo "kubectl logs job/etl-job"
3. StatefulSets : Bases de données sur Kubernetes
Les StatefulSets sont conçus pour les applications stateful (avec état) comme les bases de données.
3.1 Deployment vs StatefulSet : comprendre la différence
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ STATEFULSET : Pour applications avec état (bases de données) ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: apps/v1kind: StatefulSetmetadata:name: postgresnamespace: data-pipelinespec: # ───────────────────────────────────────────────────────────────────────── # CONFIGURATION STATEFULSET # ─────────────────────────────────────────────────────────────────────────serviceName: postgres-headless # ⭐ OBLIGATOIRE : Nom du Headless Service # Permet le DNS : postgres-0.postgres-headless.data-pipeline.svcreplicas:1 # Nombre de replicas (1 pour PostgreSQL simple) # Pour un cluster PostgreSQL : 3 (1 primary + 2 replicas)selector:matchLabels:app: postgres # Sélecteur pour identifier les pods # ───────────────────────────────────────────────────────────────────────── # TEMPLATE DU POD # ─────────────────────────────────────────────────────────────────────────template:metadata:labels:app: postgresspec:containers:-name: postgresimage: postgres:16 # Image officielle PostgreSQLports:-containerPort:5432 # Port PostgreSQL standardname: postgres # ───────────────────────────────────────────────────────────────── # VARIABLES D'ENVIRONNEMENT POSTGRESQL # ─────────────────────────────────────────────────────────────────env:-name: POSTGRES_USERvalue:"de_user" # Utilisateur de la base-name: POSTGRES_PASSWORDvalueFrom:secretKeyRef:name: postgres-secret # ⭐ Mot de passe depuis un Secretkey: password # (ne jamais mettre en clair !)-name: POSTGRES_DBvalue:"de_db" # Nom de la base créée au démarrage-name: PGDATAvalue:"/var/lib/postgresql/data/pgdata" # Où stocker les données # /pgdata est un sous-dossier pour éviter # les problèmes de permissions avec le volume # ───────────────────────────────────────────────────────────────── # MONTAGE DU VOLUME # ─────────────────────────────────────────────────────────────────volumeMounts:-name: postgres-data # Nom du volume (doit matcher volumeClaimTemplates)mountPath: /var/lib/postgresql/data # Où monter dans le container # ───────────────────────────────────────────────────────────────── # RESSOURCES # ─────────────────────────────────────────────────────────────────resources:requests:memory:"256Mi"cpu:"250m"limits:memory:"512Mi"cpu:"500m" # ───────────────────────────────────────────────────────────────────────── # VOLUME CLAIM TEMPLATES : Un PVC par pod # ───────────────────────────────────────────────────────────────────────── # C'est LA différence majeure avec un Deployment ! # Chaque pod obtient son propre PVC persistant : # - postgres-0 → postgres-data-postgres-0 # - postgres-1 → postgres-data-postgres-1 (si replicas > 1) #volumeClaimTemplates:-metadata:name: postgres-data # Nom du volume (référencé dans volumeMounts)spec:accessModes:["ReadWriteOnce"] # Un seul pod peut écrire à la foisresources:requests:storage: 5Gi # Taille du disque # storageClassName: fast-ssd # Optionnel : type de stockage---# ╔══════════════════════════════════════════════════════════════════════════╗# ║ HEADLESS SERVICE : Permet le DNS par pod ║# ╚══════════════════════════════════════════════════════════════════════════╝# # Un Headless Service (clusterIP: None) ne fait PAS de load balancing.# Il permet d'accéder directement à chaque pod via DNS :# - postgres-0.postgres-headless.data-pipeline.svc.cluster.local# - postgres-1.postgres-headless.data-pipeline.svc.cluster.local#apiVersion: v1kind: Servicemetadata:name: postgres-headless # Ce nom doit matcher spec.serviceNamenamespace: data-pipelinespec:clusterIP: None # HEADLESS = pas d'IP de clusterselector:app: postgresports:-port:5432name: postgres---# ╔══════════════════════════════════════════════════════════════════════════╗# ║ SERVICE NORMAL : Pour accéder facilement à PostgreSQL ║# ╚══════════════════════════════════════════════════════════════════════════╝## Ce service classique permet d'accéder à PostgreSQL via :# - postgres.data-pipeline.svc.cluster.local:5432# - Ou simplement "postgres" si tu es dans le même namespace#apiVersion: v1kind: Servicemetadata:name: postgresnamespace: data-pipelinespec:selector:app: postgresports:-port:5432targetPort:5432
┌─────────────────────────────────────────────────────────────────────────┐
│ COMMENT ACCÉDER À POSTGRESQL DANS LE CLUSTER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Depuis le même namespace : │
│ ───────────────────────── │
│ Host: postgres │
│ Port: 5432 │
│ │
│ Depuis un autre namespace : │
│ ────────────────────────── │
│ Host: postgres.data-pipeline.svc.cluster.local │
│ Port: 5432 │
│ │
│ Connection string Python (SQLAlchemy) : │
│ ────────────────────────────────────── │
│ postgresql://de_user:password@postgres:5432/de_db │
│ │
└─────────────────────────────────────────────────────────────────────────┘
%%bash# Vérifier l'installation de Helmecho "=== Version Helm ==="helm version --short 2>/dev/null || echo "Helm non installé"echo ""echo "=== Repositories configurés ==="helm repo list2>/dev/null || echo "Aucun repo configuré"echo ""echo "=== Releases déployées ==="helm list-A 2>/dev/null || echo "Aucune release"
5. Storage avancé pour Workloads Data
Le stockage est critique pour les workloads data. Voici les patterns recommandés.
5.1 Quel stockage pour quel usage ?
Usage
Type de volume
Caractéristiques
Base de données
PVC (StatefulSet)
Persistant, rapide (SSD)
Fichiers input
PVC ou S3/MinIO
Externalisé si possible
Fichiers output
PVC ou S3/MinIO
Externalisé pour durabilité
Données temporaires
emptyDir
Supprimé quand le pod meurt
Cache ultra-rapide
emptyDir (Memory)
RAM disk, très rapide mais volatile
5.2 emptyDir : stockage éphémère (ligne par ligne)
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ VOLUMES ÉPHÉMÈRES : Données temporaires pendant l'exécution ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: v1kind: Podmetadata:name: etl-with-temp-storagespec:containers:-name: etlimage: my-etl:1.0 # ───────────────────────────────────────────────────────────────────── # MONTAGE DES VOLUMES DANS LE CONTAINER # ─────────────────────────────────────────────────────────────────────volumeMounts:-name: tmp-data # Volume pour fichiers temporairesmountPath: /tmp/processing # Accessible dans le container ici-name: cache # Volume cache en RAMmountPath: /cache # Accessible dans le container ici # ───────────────────────────────────────────────────────────────────────── # DÉFINITION DES VOLUMES # ─────────────────────────────────────────────────────────────────────────volumes: # ───────────────────────────────────────────────────────────────────── # EMPTYDIR SUR DISQUE # ───────────────────────────────────────────────────────────────────── # - Créé quand le pod démarre # - Supprimé quand le pod meurt (même si le container restart) # - Stocké sur le disque du node # - Parfait pour : fichiers intermédiaires, décompression, etc. #-name: tmp-dataemptyDir:{} # {} = valeurs par défaut (disque du node) # ───────────────────────────────────────────────────────────────────── # EMPTYDIR EN RAM (ULTRA-RAPIDE) # ───────────────────────────────────────────────────────────────────── # - Stocké en RAM (pas sur disque) # - TRÈS rapide (lecture/écriture) # - ATTENTION : compte dans la limite mémoire du pod ! # - Parfait pour : cache, données fréquemment accédées #-name: cacheemptyDir:medium: Memory # ⭐ Stocké en RAM au lieu du disquesizeLimit: 256Mi # Limite de taille (compte dans limits.memory !)
┌─────────────────────────────────────────────────────────────────────────┐
│ EMPTYDIR : CYCLE DE VIE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Pod démarre │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ emptyDir │ ← Volume créé (vide) │
│ │ créé │ │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ Container écrit des fichiers... │
│ │ │
│ ▼ │
│ Container crash et redémarre │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Données │ ← Les données sont PRÉSERVÉES │
│ │ intactes │ (tant que le POD existe) │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ Pod supprimé │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ emptyDir │ ← Volume SUPPRIMÉ définitivement │
│ │ supprimé │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5.3 PVC avec StorageClass (ligne par ligne)
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ PVC : Demande de stockage persistant ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: v1kind: PersistentVolumeClaimmetadata:name: etl-data-pvcnamespace: data-pipelinespec: # ───────────────────────────────────────────────────────────────────────── # STORAGE CLASS (quel type de stockage ?) # ───────────────────────────────────────────────────────────────────────── # La StorageClass définit : # - Le type de disque (SSD, HDD) # - Le provisioner (AWS EBS, GCP PD, Azure Disk, etc.) # - Les options de réplication # # Pour voir les StorageClasses disponibles : # kubectl get storageclasses #storageClassName: standard # "standard" = souvent le défaut # "fast-ssd", "premium-ssd" selon ton cloud # Omit = utilise la StorageClass par défaut # ───────────────────────────────────────────────────────────────────────── # MODE D'ACCÈS # ─────────────────────────────────────────────────────────────────────────accessModes:- ReadWriteOnce # RWO = 1 seul node peut monter en écriture # ReadWriteMany (RWX) = plusieurs nodes # ReadOnlyMany (ROX) = plusieurs en lecture seule # # ⚠️ RWX n'est pas supporté par tous les providers ! # ───────────────────────────────────────────────────────────────────────── # TAILLE DEMANDÉE # ─────────────────────────────────────────────────────────────────────────resources:requests:storage: 10Gi # 10 Go de stockage
# Commandes utiles pour le storagekubectl get storageclasses # Voir les classes disponibleskubectl get pvc -n data-pipeline # Voir les PVCkubectl get pv # Voir les PV (volumes provisionnés)kubectl describe pvc etl-data-pvc -n data-pipeline # Détails d'un PVC
6. Scaling & Gestion des ressources
Les workloads data sont souvent gourmands en ressources. Voici comment les gérer.
6.1 Requests vs Limits : comprendre la différence
resources:requests: # MINIMUM garantimemory:"512Mi" # K8s réserve 512 Mo pour ce podcpu:"500m" # K8s réserve 0.5 CPU pour ce podlimits: # MAXIMUM autorisémemory:"2Gi" # Si dépassé → OOMKilled (pod tué)cpu:"2000m" # Si dépassé → Throttling (ralenti)
Type
Ce que ça fait
Conséquence si dépassé
requests
Minimum garanti par K8s
Pod reste en Pending si pas assez de ressources sur le cluster
limits.memory
Maximum de RAM
OOMKilled : le pod est tué immédiatement
limits.cpu
Maximum de CPU
Throttling : le pod est ralenti (pas tué)
6.2 HorizontalPodAutoscaler (ligne par ligne)
Le HPA scale automatiquement le nombre de pods selon les métriques.
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ HPA : Scaling automatique basé sur les métriques ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata:name: etl-worker-hpanamespace: data-pipelinespec: # ───────────────────────────────────────────────────────────────────────── # CIBLE : Quel Deployment scaler ? # ─────────────────────────────────────────────────────────────────────────scaleTargetRef:apiVersion: apps/v1kind: Deployment # Type de ressource à scalername: etl-worker # Nom du Deployment # ───────────────────────────────────────────────────────────────────────── # LIMITES DE SCALING # ─────────────────────────────────────────────────────────────────────────minReplicas:2 # Minimum 2 pods (même si charge faible)maxReplicas:10 # Maximum 10 pods (limite les coûts) # ───────────────────────────────────────────────────────────────────────── # MÉTRIQUES DE DÉCISION # ─────────────────────────────────────────────────────────────────────────metrics: # Métrique CPU-type: Resourceresource:name: cputarget:type: UtilizationaverageUtilization:70 # Si CPU > 70% en moyenne → scale UP # Si CPU < 70% en moyenne → scale DOWN # Métrique Mémoire-type: Resourceresource:name: memorytarget:type: UtilizationaverageUtilization:80 # Si RAM > 80% en moyenne → scale UP
┌─────────────────────────────────────────────────────────────────────────┐
│ FONCTIONNEMENT DU HPA │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CPU actuel : 30% CPU cible : 70% Replicas : 2 │
│ ───────────────────────────────────────────────────── │
│ → Charge faible, on reste à 2 replicas (minReplicas) │
│ │
│ CPU actuel : 85% CPU cible : 70% Replicas : 2 │
│ ───────────────────────────────────────────────────── │
│ → CPU > 70% ! HPA ajoute des pods → 3 replicas │
│ │
│ CPU actuel : 90% CPU cible : 70% Replicas : 3 │
│ ───────────────────────────────────────────────────── │
│ → Toujours > 70% ! HPA ajoute encore → 4 replicas │
│ │
│ CPU actuel : 60% CPU cible : 70% Replicas : 4 │
│ ───────────────────────────────────────────────────── │
│ → CPU < 70%, on attend un peu (cooldown) │
│ → Puis HPA réduit → 3 replicas │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.3 ResourceQuota par namespace (ligne par ligne)
Limite les ressources consommables par namespace (utile pour les équipes).
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ RESOURCEQUOTA : Limites globales par namespace ║# ╚══════════════════════════════════════════════════════════════════════════╝apiVersion: v1kind: ResourceQuotametadata:name: data-team-quotanamespace: data-pipeline # S'applique à ce namespace uniquementspec:hard: # Limites "dures" (ne peuvent pas être dépassées) # ───────────────────────────────────────────────────────────────────── # LIMITES CPU # ─────────────────────────────────────────────────────────────────────requests.cpu:"10" # Total des requests CPU dans le namespace # = maximum 10 CPU réservéslimits.cpu:"20" # Total des limits CPU # = maximum 20 CPU utilisables # ───────────────────────────────────────────────────────────────────── # LIMITES MÉMOIRE # ─────────────────────────────────────────────────────────────────────requests.memory:"20Gi" # Total des requests mémoire = 20 Golimits.memory:"40Gi" # Total des limits mémoire = 40 Go # ───────────────────────────────────────────────────────────────────── # LIMITES D'OBJETS # ─────────────────────────────────────────────────────────────────────pods:"50" # Maximum 50 pods dans ce namespacepersistentvolumeclaims:"10" # Maximum 10 PVCservices:"20" # Maximum 20 services
# Voir les quotas et leur utilisationkubectl describe resourcequota data-team-quota -n data-pipeline
6.4 Node Affinity : placer les pods sur des nodes spécifiques
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ NODE AFFINITY : Contrôler où les pods sont placés ║# ╚══════════════════════════════════════════════════════════════════════════╝spec:affinity:nodeAffinity: # ───────────────────────────────────────────────────────────────────── # RÈGLE OBLIGATOIRE (required) # ───────────────────────────────────────────────────────────────────── # Le pod NE SERA PAS schedulé si aucun node ne match #requiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:-matchExpressions:-key: node-type # Label du nodeoperator: In # In, NotIn, Exists, DoesNotExist, Gt, Ltvalues:- high-memory # Nodes avec label "node-type=high-memory"- high-cpu # Ou "node-type=high-cpu" # ───────────────────────────────────────────────────────────────────── # RÈGLE PRÉFÉRÉE (preferred) # ───────────────────────────────────────────────────────────────────── # Le scheduler PRÉFÈRE ces nodes, mais peut en choisir d'autres #preferredDuringSchedulingIgnoredDuringExecution:-weight:100 # Poids de la préférence (1-100)preference:matchExpressions:-key: zoneoperator: Invalues:- eu-west-1a # Préfère les nodes dans eu-west-1a
# Voir les labels des nodeskubectl get nodes --show-labels# Ajouter un label à un nodekubectl label nodes node-1 node-type=high-memory
6.5 Taints & Tolerations : réserver des nodes
# ─────────────────────────────────────────────────────────────────────────# TAINT : "Marquer" un node pour le réserver# ─────────────────────────────────────────────────────────────────────────# Seuls les pods avec la Toleration correspondante peuvent y allerkubectl taint nodes node-1 workload=data:NoSchedule# ▲ ▲# │ └── Effet : NoSchedule, PreferNoSchedule, NoExecute# └── Clé=Valeur
# ╔══════════════════════════════════════════════════════════════════════════╗# ║ TOLERATION : Permet au pod d'aller sur un node "tainté" ║# ╚══════════════════════════════════════════════════════════════════════════╝spec:tolerations:-key:"workload" # Clé du taintoperator:"Equal" # Equal (clé=valeur) ou Exists (clé présente)value:"data" # Valeur du tainteffect:"NoSchedule" # Effet à tolérer
┌─────────────────────────────────────────────────────────────────────────┐
│ TAINTS & TOLERATIONS : EXEMPLE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Node-1 (taint: workload=data:NoSchedule) │
│ ───────────────────────────────────────── │
│ │ │
│ ├── Pod ETL (avec toleration) ✅ → Peut être schedulé │
│ │ │
│ └── Pod Web (sans toleration) ❌ → Ne peut PAS être schedulé │
│ │
│ Node-2 (pas de taint) │
│ ───────────────────── │
│ │ │
│ ├── Pod ETL ✅ → Peut être schedulé │
│ │ │
│ └── Pod Web ✅ → Peut être schedulé │
│ │
│ → Les nodes "taintés" sont RÉSERVÉS aux pods data ! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
7. Monitoring & Observabilité
Surveiller tes workloads data est essentiel pour détecter les problèmes.
7.1 Métriques de base avec kubectl
# CPU/RAM des pods (nécessite metrics-server)kubectl top podskubectl top pods -n data-pipeline# CPU/RAM des nodeskubectl top nodes# Events (problèmes récents)kubectl get events --sort-by='.lastTimestamp'kubectl get events -n data-pipeline
7.2 Prometheus + Grafana (aperçu)
La stack Prometheus + Grafana est le standard pour le monitoring K8s :
Outil
Rôle
Prometheus
Collecte et stocke les métriques
Grafana
Visualisation et dashboards
AlertManager
Alertes (Slack, email, PagerDuty)
# Installation rapide via Helmhelm repo add prometheus-community https://prometheus-community.github.io/helm-chartshelm install monitoring prometheus-community/kube-prometheus-stack \--namespace monitoring \--create-namespace
7.3 Logs
# Logs d'un podkubectl logs <pod>kubectl logs -f<pod># Followkubectl logs -p<pod># Previous (après crash)kubectl logs --tail=100 <pod># 100 dernières lignes# Logs d'un Jobkubectl logs job/<job-name># Logs de tous les pods d'un labelkubectl logs -l app=etl --all-containers
7.4 Centralisation des logs
Pour les environnements de production, centralise les logs avec :
Solution
Description
ELK Stack
Elasticsearch + Logstash + Kibana
Loki + Grafana
Solution légère (recommandée)
Cloud native
CloudWatch (AWS), Cloud Logging (GCP)
7.5 Dashboards utiles pour Data Engineering
Dashboard
Métriques
K8s Cluster Overview
CPU, RAM, pods par node
Job Success Rate
Jobs succeeded vs failed
Pod Resource Usage
Consommation vs requests/limits
PVC Usage
Espace disque utilisé
8. Aperçu : Spark & Airflow sur Kubernetes
⚠️ Note : Cette section est un aperçu pour te donner une vision d’ensemble. - Spark sera détaillé dans les modules 19-22 - Airflow sera détaillé dans le module 25
8.1 Spark on Kubernetes (aperçu)
Pourquoi Spark sur K8s ? - Pas besoin de cluster YARN ou Mesos dédié - Élasticité native (pods créés à la demande) - Parfait pour jobs ETL batch éphémères - Intégration cloud-native
Architecture simplifiée :
┌─────────────────┐
│ spark-submit │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ Driver Pod │────────▶│ Executor Pod │ x N
│ (coordonne) │ │ (traitement) │
└─────────────────┘ └─────────────────┘
Exemple de commande (aperçu) :
# Tu verras ça en détail dans le module 19spark-submit\--master k8s://https://<K8S_API>\--deploy-mode cluster \--conf spark.kubernetes.container.image=spark:3.5 \--conf spark.executor.instances=3 \ local:///opt/spark/jobs/etl.py
8.2 Airflow on Kubernetes (aperçu)
Pourquoi Airflow sur K8s ? - Scalabilité des workers - Isolation parfaite des tâches - KubernetesExecutor : 1 tâche = 1 pod
Architectures possibles :
Executor
Description
Usage
LocalExecutor
Tout dans 1 pod
Dev/test
CeleryExecutor
Workers via Redis
Production classique
KubernetesExecutor
1 pod par tâche
Production K8s native
Déploiement via Helm (aperçu) :
# Tu verras ça en détail dans le module 25helm repo add apache-airflow https://airflow.apache.orghelm install airflow apache-airflow/airflow \--namespace airflow \--set executor=KubernetesExecutor
KubernetesPodOperator (aperçu) :
# Exécuter une tâche dans un pod K8s dédiéfrom airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperatoretl_task = KubernetesPodOperator( task_id="etl_task", namespace="data-pipeline", image="my-etl:1.0", cmds=["python", "etl.py"], name="etl-pod",)
💡 Après avoir terminé les modules Spark (19-22), reviens sur cette section pour mieux comprendre l’intégration K8s !
9. Erreurs fréquentes & Bonnes pratiques
❌ Erreurs fréquentes
Erreur
Cause
Solution
OOMKilled
Mémoire insuffisante
Augmenter limits.memory
DeadlineExceeded
Job trop long
Augmenter activeDeadlineSeconds
Pending (Job)
Pas de ressources disponibles
Vérifier quotas, réduire requests
CrashLoopBackOff
App plante au démarrage
kubectl logs <pod>
ImagePullBackOff
Image/registry incorrects
Vérifier image, imagePullSecrets
PVC Pending
Pas de PV disponible
Vérifier StorageClass
Job jamais nettoyé
Pas de TTL
Ajouter ttlSecondsAfterFinished
✅ Bonnes pratiques pour workloads data
Pratique
Pourquoi
Toujours définir resources
Évite OOM et problèmes de scheduling
TTL sur les Jobs
Nettoyage automatique
Logs externalisés
Ne pas dépendre des logs K8s
ConfigMaps pour paramètres
Pas de hardcoding
Secrets pour credentials
Sécurité
Labels systématiques
Filtrage et monitoring
Namespace par projet
Isolation, quotas
Healthchecks
K8s sait si l’app est prête
Labels recommandés pour workloads data
metadata:labels:app: etl-pipelinecomponent: transform # extract, transform, loadenv: productionteam: data-engineeringversion:"1.2.0"schedule: daily # Pour les CronJobs
Quiz de fin de module
Réponds aux questions suivantes pour vérifier tes acquis.
❓ Q1. Qu’est-ce qu’un “workload data” ?
Une application web qui affiche des données
Une charge de travail dédiée au traitement, transformation ou déplacement de données
Un dashboard de visualisation
Une base de données
💡 Voir la réponse
✅ Réponse : b — Un workload data est une tâche liée au traitement de données : ETL, transformations, ingestion, etc.
❓ Q2. Quel paramètre permet de nettoyer automatiquement un Job terminé après 24h ?
backoffLimit: 24
ttlSecondsAfterFinished: 86400
activeDeadlineSeconds: 86400
cleanupAfter: 24h
💡 Voir la réponse
✅ Réponse : b — ttlSecondsAfterFinished: 86400 supprime automatiquement le Job 24h après sa completion.
❓ Q3. Quelle est la différence principale entre Deployment et StatefulSet ?
Deployment est plus récent
StatefulSet garantit une identité stable et un stockage persistant par pod
Deployment ne supporte pas les volumes
StatefulSet est uniquement pour les bases NoSQL
💡 Voir la réponse
✅ Réponse : b — StatefulSet donne une identité stable (pod-0, pod-1) et un PVC par pod, idéal pour les bases de données.
❓ Q4. Qu’est-ce que Helm ?
Un outil de monitoring
Un package manager pour Kubernetes
Un orchestrateur de containers
Un système de logs
💡 Voir la réponse
✅ Réponse : b — Helm est le package manager de K8s, permettant d’installer des applications complexes avec une seule commande.
❓ Q5. Que signifie l’erreur OOMKilled ?
Le pod a été tué par l’administrateur
L’image Docker est corrompue
Le container a dépassé sa limite de mémoire
Le réseau est indisponible
💡 Voir la réponse
✅ Réponse : c — OOMKilled (Out Of Memory Killed) signifie que le container a dépassé limits.memory et a été tué par K8s.
❓ Q6. Quel executor Airflow crée un pod K8s par tâche ?
LocalExecutor
CeleryExecutor
KubernetesExecutor
SequentialExecutor
💡 Voir la réponse
✅ Réponse : c — Le KubernetesExecutor lance chaque tâche Airflow dans un pod K8s dédié.
❓ Q7. Quelle QoS class offre la meilleure protection contre l’éviction ?
BestEffort
Burstable
Guaranteed
Protected
💡 Voir la réponse
✅ Réponse : c — Les pods Guaranteed (requests = limits) sont les derniers à être évincés en cas de pression sur les ressources.
Mini-projet : Pipeline ETL Python sur Kubernetes
Objectif
Déployer un pipeline ETL batch complet sur Kubernetes, sans Spark (Python/pandas), avec : - MinIO : stockage des fichiers source (S3-compatible) - PostgreSQL : destination des données - CronJob : ETL planifié quotidiennement
apiVersion: batch/v1kind: CronJobmetadata:name: etl-dailynamespace: data-pipelinelabels:app: etl-pipelineschedule: dailyspec:schedule:"0 2 * * *" # Tous les jours à 2hconcurrencyPolicy: ForbidsuccessfulJobsHistoryLimit:3failedJobsHistoryLimit:2jobTemplate:spec:backoffLimit:3activeDeadlineSeconds:1800ttlSecondsAfterFinished:86400template:metadata:labels:app: etl-jobspec:containers:-name: etlimage: my-etl:1.0 # Remplacer par ton imageenvFrom:-configMapRef:name: etl-config-secretRef:name: etl-secretresources:requests:memory:"256Mi"cpu:"200m"limits:memory:"512Mi"cpu:"500m"restartPolicy: OnFailure