Kubernetes pour Workloads Data

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 !


Prérequis

Niveau Compétence
✅ Requis Avoir suivi le module 15_kubernetes_fundamentals
✅ Requis Maîtriser Pod, Deployment, Service, ConfigMap, Secret, PVC
✅ Requis Savoir utiliser kubectl
💡 Recommandé 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 CronJobs
kind: Job                         # Type = Job (tâche one-shot)

metadata:
  name: etl-advanced-job          # Nom unique du job
  namespace: data-pipeline        # Namespace cible
  labels:
    app: etl                      # Labels pour filtrer/monitorer
    team: 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 fois
  
  parallelism: 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 erreur
  
  activeDeadlineSeconds: 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 container
        image: my-etl:1.0         # Image Docker
        
        # Ressources (TOUJOURS les définir pour les Jobs !)
        resources:
          requests:               # Minimum garanti
            memory: "512Mi"       # 512 Mo de RAM minimum
            cpu: "500m"           # 0.5 CPU minimum
          limits:                 # 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/v1
kind: Job
metadata:
  name: etl-parallel
  namespace: data-pipeline

spec:
  completions: 10                 # 10 fichiers à traiter = 10 succès requis
  parallelism: 3                  # 3 pods tournent en même temps
  
  completionMode: 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: etl
        image: my-etl:1.0
        command: ["python", "etl.py"]
        
        env:
        # ⭐ Récupérer l'index du pod (0, 1, 2, ..., 9)
        - name: FILE_INDEX
          valueFrom:
            fieldRef:             # Référence à un champ du pod lui-même
              fieldPath: 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/v1
kind: CronJob
metadata:
  name: etl-daily
  namespace: data-pipeline

spec:
  # ─────────────────────────────────────────────────────────────────────────
  # 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éussis
  failedJobsHistoryLimit: 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 échec
      activeDeadlineSeconds: 3600 # Timeout 1h
      
      template:
        spec:
          containers:
          - name: etl
            image: my-etl:1.0
            
            # Charger la config depuis un ConfigMap
            envFrom:
            - configMapRef:
                name: etl-config  # Toutes les clés du ConfigMap
                                  # deviennent des variables d'environnement
            
            resources:
              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 Jobs
kubectl get jobs -n data-pipeline

# Lister les CronJobs
kubectl get cronjobs -n data-pipeline

# Voir les détails d'un CronJob
kubectl 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 job
kubectl logs job/test-etl -n data-pipeline

# Supprimer un job
kubectl delete job test-etl -n data-pipeline
Voir le code
%%bash
# Commandes utiles pour les Jobs

echo "=== Lister les Jobs ==="
kubectl get jobs

echo ""
echo "=== Lister les CronJobs ==="
kubectl get cronjobs

echo ""
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

Aspect Deployment StatefulSet
Nom des pods Aléatoire (app-7d8f9...) Stable et ordonné (app-0, app-1, app-2)
Stockage PVC partagé ou éphémère PVC unique par pod (persistant)
Ordre de démarrage Tous en parallèle Séquentiel : 0 → 1 → 2
Ordre d’arrêt Tous en parallèle Inverse : 2 → 1 → 0
Réseau Service ClusterIP classique Headless Service (DNS par pod)
Usage typique Apps stateless (API, web) Bases de données, caches, queues
┌─────────────────────────────────────────────────────────────────────────┐
│  DEPLOYMENT vs STATEFULSET                                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  DEPLOYMENT (stateless)          STATEFULSET (stateful)                │
│  ─────────────────────          ──────────────────────                 │
│                                                                         │
│  ┌─────────────┐                ┌─────────────┐                        │
│  │ app-7d8f9.. │                │   app-0     │ ← Identité stable      │
│  └─────────────┘                └──────┬──────┘                        │
│  ┌─────────────┐                       │                               │
│  │ app-x2k4m.. │                ┌──────▼──────┐                        │
│  └─────────────┘                │   PVC-0     │ ← Volume dédié         │
│  ┌─────────────┐                └─────────────┘                        │
│  │ app-p9n3q.. │                                                       │
│  └─────────────┘                ┌─────────────┐                        │
│                                  │   app-1     │                        │
│  Tous partagent le même PVC     └──────┬──────┘                        │
│  (ou pas de PVC)                       │                               │
│                                  ┌──────▼──────┐                        │
│                                  │   PVC-1     │                        │
│                                  └─────────────┘                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.2 PostgreSQL avec StatefulSet (ligne par ligne)

# ╔══════════════════════════════════════════════════════════════════════════╗
# ║  STATEFULSET : Pour applications avec état (bases de données)            ║
# ╚══════════════════════════════════════════════════════════════════════════╝
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: data-pipeline

spec:
  # ─────────────────────────────────────────────────────────────────────────
  # CONFIGURATION STATEFULSET
  # ─────────────────────────────────────────────────────────────────────────
  serviceName: postgres-headless  # ⭐ OBLIGATOIRE : Nom du Headless Service
                                  # Permet le DNS : postgres-0.postgres-headless.data-pipeline.svc
  
  replicas: 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: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16        # Image officielle PostgreSQL
        
        ports:
        - containerPort: 5432     # Port PostgreSQL standard
          name: postgres
        
        # ─────────────────────────────────────────────────────────────────
        # VARIABLES D'ENVIRONNEMENT POSTGRESQL
        # ─────────────────────────────────────────────────────────────────
        env:
        - name: POSTGRES_USER
          value: "de_user"        # Utilisateur de la base
        
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret  # ⭐ Mot de passe depuis un Secret
              key: password          # (ne jamais mettre en clair !)
        
        - name: POSTGRES_DB
          value: "de_db"          # Nom de la base créée au démarrage
        
        - name: PGDATA
          value: "/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 fois
      resources:
        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: v1
kind: Service
metadata:
  name: postgres-headless         # Ce nom doit matcher spec.serviceName
  namespace: data-pipeline
spec:
  clusterIP: None                 # HEADLESS = pas d'IP de cluster
  selector:
    app: postgres
  ports:
  - port: 5432
    name: 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: v1
kind: Service
metadata:
  name: postgres
  namespace: data-pipeline
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 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                     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.3 Le Secret pour le mot de passe

# postgres-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: data-pipeline
type: Opaque
stringData:                       # stringData = K8s encode en base64 automatiquement
  password: "SuperSecretPassword123"

⚠️ Recommandation production

En production, préfère les services managés : - AWS RDS / Aurora - GCP Cloud SQL
- Azure Database for PostgreSQL

Pourquoi ? - Haute disponibilité automatique - Backups automatiques - Patches de sécurité - Scaling facile

Les StatefulSets sont parfaits pour : - Développement local - Tests d’intégration - Environnements où tu veux tout contrôler


4. Helm : Package Manager Kubernetes

Helm est le gestionnaire de packages pour Kubernetes — comme apt pour Ubuntu ou pip pour Python.

4.1 Pourquoi Helm ?

Sans Helm Avec Helm
10+ fichiers YAML à gérer 1 commande helm install
Copier-coller entre environnements values.yaml pour personnaliser
Pas de versioning Rollback facile
Mise à jour manuelle helm upgrade

4.2 Concepts clés

Concept Description Analogie
Chart Package K8s (templates + values) Un package .deb ou .rpm
Release Instance déployée d’un chart Une installation du package
Repository Magasin de charts Un apt repository
Values Configuration personnalisée Un fichier de config

4.3 Installation de Helm

# macOS
brew install helm

# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Vérifier
helm version

4.4 Commandes essentielles

# Ajouter un repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Rechercher un chart
helm search repo postgresql

# Voir les valeurs par défaut
helm show values bitnami/postgresql

# Installer un chart
helm install my-postgres bitnami/postgresql \
  --namespace data \
  --create-namespace \
  --set auth.postgresPassword=mypassword

# Lister les releases
helm list -A

# Mettre à jour
helm upgrade my-postgres bitnami/postgresql --set auth.postgresPassword=newpassword

# Rollback
helm rollback my-postgres 1

# Désinstaller
helm uninstall my-postgres -n data

4.5 Fichier values.yaml personnalisé

# postgres-values.yaml
auth:
  postgresPassword: "de_password"
  database: "de_db"

primary:
  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
    limits:
      memory: "512Mi"
      cpu: "500m"
  persistence:
    size: 5Gi
# Installer avec le fichier values
helm install my-postgres bitnami/postgresql -f postgres-values.yaml

4.6 Charts utiles pour Data Engineering

Chart Repository Usage
bitnami/postgresql bitnami Base de données relationnelle
bitnami/redis bitnami Cache, broker de messages
minio/minio minio Object storage S3-compatible
apache-airflow/airflow apache-airflow Orchestration (module 25)
bitnami/spark bitnami Spark cluster (module 19)
# Ajouter les repos utiles
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add minio https://charts.min.io/
helm repo add apache-airflow https://airflow.apache.org
helm repo update
Voir le code
%%bash
# Vérifier l'installation de Helm
echo "=== Version Helm ==="
helm version --short 2>/dev/null || echo "Helm non installé"

echo ""
echo "=== Repositories configurés ==="
helm repo list 2>/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: v1
kind: Pod
metadata:
  name: etl-with-temp-storage
spec:
  containers:
  - name: etl
    image: my-etl:1.0
    
    # ─────────────────────────────────────────────────────────────────────
    # MONTAGE DES VOLUMES DANS LE CONTAINER
    # ─────────────────────────────────────────────────────────────────────
    volumeMounts:
    - name: tmp-data              # Volume pour fichiers temporaires
      mountPath: /tmp/processing  # Accessible dans le container ici
    
    - name: cache                 # Volume cache en RAM
      mountPath: /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-data
    emptyDir: {}                  # {} = 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: cache
    emptyDir:
      medium: Memory              # ⭐ Stocké en RAM au lieu du disque
      sizeLimit: 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: v1
kind: PersistentVolumeClaim
metadata:
  name: etl-data-pvc
  namespace: data-pipeline

spec:
  # ─────────────────────────────────────────────────────────────────────────
  # 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 storage
kubectl get storageclasses                    # Voir les classes disponibles
kubectl get pvc -n data-pipeline              # Voir les PVC
kubectl 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 garanti
    memory: "512Mi"               # K8s réserve 512 Mo pour ce pod
    cpu: "500m"                   # K8s réserve 0.5 CPU pour ce pod
  limits:                         # 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/v2
kind: HorizontalPodAutoscaler
metadata:
  name: etl-worker-hpa
  namespace: data-pipeline

spec:
  # ─────────────────────────────────────────────────────────────────────────
  # CIBLE : Quel Deployment scaler ?
  # ─────────────────────────────────────────────────────────────────────────
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment              # Type de ressource à scaler
    name: 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: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70    # Si CPU > 70% en moyenne → scale UP
                                  # Si CPU < 70% en moyenne → scale DOWN
  
  # Métrique Mémoire
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 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: v1
kind: ResourceQuota
metadata:
  name: data-team-quota
  namespace: data-pipeline        # S'applique à ce namespace uniquement

spec:
  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és
    limits.cpu: "20"              # Total des limits CPU
                                  # = maximum 20 CPU utilisables
    
    # ─────────────────────────────────────────────────────────────────────
    # LIMITES MÉMOIRE
    # ─────────────────────────────────────────────────────────────────────
    requests.memory: "20Gi"       # Total des requests mémoire = 20 Go
    limits.memory: "40Gi"         # Total des limits mémoire = 40 Go
    
    # ─────────────────────────────────────────────────────────────────────
    # LIMITES D'OBJETS
    # ─────────────────────────────────────────────────────────────────────
    pods: "50"                    # Maximum 50 pods dans ce namespace
    persistentvolumeclaims: "10"  # Maximum 10 PVC
    services: "20"                # Maximum 20 services
# Voir les quotas et leur utilisation
kubectl 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 node
            operator: In          # In, NotIn, Exists, DoesNotExist, Gt, Lt
            values:
            - 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: zone
            operator: In
            values:
            - eu-west-1a          # Préfère les nodes dans eu-west-1a
# Voir les labels des nodes
kubectl get nodes --show-labels

# Ajouter un label à un node
kubectl 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 aller

kubectl 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 taint
    operator: "Equal"             # Equal (clé=valeur) ou Exists (clé présente)
    value: "data"                 # Valeur du taint
    effect: "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 pods
kubectl top pods -n data-pipeline

# CPU/RAM des nodes
kubectl 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 Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace

7.3 Logs

# Logs d'un pod
kubectl logs <pod>
kubectl logs -f <pod>           # Follow
kubectl logs -p <pod>           # Previous (après crash)
kubectl logs --tail=100 <pod>   # 100 dernières lignes

# Logs d'un Job
kubectl logs job/<job-name>

# Logs de tous les pods d'un label
kubectl 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 19
spark-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 25
helm repo add apache-airflow https://airflow.apache.org
helm 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 KubernetesPodOperator

etl_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-pipeline
    component: transform    # extract, transform, load
    env: production
    team: data-engineering
    version: "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” ?

  1. Une application web qui affiche des données
  2. Une charge de travail dédiée au traitement, transformation ou déplacement de données
  3. Un dashboard de visualisation
  4. 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 ?

  1. backoffLimit: 24
  2. ttlSecondsAfterFinished: 86400
  3. activeDeadlineSeconds: 86400
  4. cleanupAfter: 24h
💡 Voir la réponse

Réponse : bttlSecondsAfterFinished: 86400 supprime automatiquement le Job 24h après sa completion.


❓ Q3. Quelle est la différence principale entre Deployment et StatefulSet ?

  1. Deployment est plus récent
  2. StatefulSet garantit une identité stable et un stockage persistant par pod
  3. Deployment ne supporte pas les volumes
  4. 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 ?

  1. Un outil de monitoring
  2. Un package manager pour Kubernetes
  3. Un orchestrateur de containers
  4. 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 ?

  1. Le pod a été tué par l’administrateur
  2. L’image Docker est corrompue
  3. Le container a dépassé sa limite de mémoire
  4. 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 ?

  1. LocalExecutor
  2. CeleryExecutor
  3. KubernetesExecutor
  4. 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 ?

  1. BestEffort
  2. Burstable
  3. Guaranteed
  4. 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

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│      MinIO      │────▶│   CronJob ETL   │────▶│   PostgreSQL    │
│  (S3 / input)   │     │    (Python)     │     │    (output)     │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                       │                       │
        └───── Helm ────────────┴───── Manifests ───────┘

Structure du projet

k8s-etl-project/
├── helm-values/
│   ├── minio-values.yaml
│   └── postgres-values.yaml
├── manifests/
│   ├── namespace.yaml
│   ├── etl-configmap.yaml
│   ├── etl-secret.yaml
│   └── etl-cronjob.yaml
├── etl/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── etl.py
├── data/
│   └── sales.csv
└── README.md

Étapes

  1. Créer le namespace data-pipeline
  2. Déployer MinIO via Helm
  3. Déployer PostgreSQL via Helm
  4. Uploader les données dans MinIO
  5. Build & push l’image ETL
  6. Déployer le CronJob
  7. Tester manuellement
  8. Vérifier les données dans PostgreSQL

✅ Solution du mini-projet

📥 Afficher la solution complète

1. manifests/namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: data-pipeline
  labels:
    team: data-engineering
    project: etl-demo

2. helm-values/minio-values.yaml

rootUser: admin
rootPassword: minio123456
persistence:
  size: 5Gi
resources:
  requests:
    memory: "256Mi"
    cpu: "100m"

3. helm-values/postgres-values.yaml

auth:
  postgresPassword: "postgres123"
  username: "de_user"
  password: "de_password"
  database: "de_db"
primary:
  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
  persistence:
    size: 2Gi

4. manifests/etl-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: etl-config
  namespace: data-pipeline
data:
  MINIO_ENDPOINT: "minio.data-pipeline.svc.cluster.local:9000"
  MINIO_BUCKET: "raw-data"
  MINIO_FILE: "sales.csv"
  DB_HOST: "postgres-postgresql.data-pipeline.svc.cluster.local"
  DB_PORT: "5432"
  DB_NAME: "de_db"
  DB_USER: "de_user"

5. manifests/etl-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: etl-secret
  namespace: data-pipeline
type: Opaque
stringData:
  MINIO_ACCESS_KEY: "admin"
  MINIO_SECRET_KEY: "minio123456"
  DB_PASSWORD: "de_password"

6. manifests/etl-cronjob.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: etl-daily
  namespace: data-pipeline
  labels:
    app: etl-pipeline
    schedule: daily
spec:
  schedule: "0 2 * * *"  # Tous les jours à 2h
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 2
  jobTemplate:
    spec:
      backoffLimit: 3
      activeDeadlineSeconds: 1800
      ttlSecondsAfterFinished: 86400
      template:
        metadata:
          labels:
            app: etl-job
        spec:
          containers:
          - name: etl
            image: my-etl:1.0  # Remplacer par ton image
            envFrom:
            - configMapRef:
                name: etl-config
            - secretRef:
                name: etl-secret
            resources:
              requests:
                memory: "256Mi"
                cpu: "200m"
              limits:
                memory: "512Mi"
                cpu: "500m"
          restartPolicy: OnFailure

7. etl/requirements.txt

pandas==2.1.4
boto3==1.34.0
psycopg2-binary==2.9.9
sqlalchemy==2.0.25

8. etl/Dockerfile

FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY etl.py .

CMD ["python", "etl.py"]

9. etl/etl.py

import os
import pandas as pd
import boto3
from sqlalchemy import create_engine
from io import BytesIO

def main():
    print("🚀 Démarrage ETL...")
    
    # Config MinIO
    minio_endpoint = os.environ['MINIO_ENDPOINT']
    minio_access = os.environ['MINIO_ACCESS_KEY']
    minio_secret = os.environ['MINIO_SECRET_KEY']
    bucket = os.environ['MINIO_BUCKET']
    file_key = os.environ['MINIO_FILE']
    
    # Config PostgreSQL
    db_host = os.environ['DB_HOST']
    db_port = os.environ['DB_PORT']
    db_name = os.environ['DB_NAME']
    db_user = os.environ['DB_USER']
    db_pass = os.environ['DB_PASSWORD']
    
    # EXTRACT : Lire depuis MinIO
    print(f"📥 Lecture depuis MinIO: {bucket}/{file_key}")
    s3 = boto3.client(
        's3',
        endpoint_url=f'http://{minio_endpoint}',
        aws_access_key_id=minio_access,
        aws_secret_access_key=minio_secret
    )
    
    response = s3.get_object(Bucket=bucket, Key=file_key)
    df = pd.read_csv(BytesIO(response['Body'].read()))
    print(f"   {len(df)} lignes lues")
    
    # TRANSFORM
    print("🔄 Transformation...")
    df['total'] = df['quantity'] * df['price']
    df['loaded_at'] = pd.Timestamp.now()
    
    # LOAD : Écrire dans PostgreSQL
    print(f"📤 Chargement dans PostgreSQL...")
    engine = create_engine(
        f'postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}'
    )
    df.to_sql('sales', engine, if_exists='replace', index=False)
    
    print("✅ ETL terminé avec succès !")
    print(df.head())

if __name__ == "__main__":
    main()

10. Commandes de déploiement

# 1. Créer le namespace
kubectl apply -f manifests/namespace.yaml

# 2. Installer MinIO
helm repo add minio https://charts.min.io/
helm install minio minio/minio \
  -n data-pipeline \
  -f helm-values/minio-values.yaml

# 3. Installer PostgreSQL
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql \
  -n data-pipeline \
  -f helm-values/postgres-values.yaml

# 4. Attendre que tout soit prêt
kubectl get pods -n data-pipeline -w

# 5. Upload data dans MinIO (via port-forward)
kubectl port-forward svc/minio -n data-pipeline 9000:9000 &
# Puis utiliser mc (MinIO client) ou l'UI

# 6. Build & push image ETL
cd etl
docker build -t my-etl:1.0 .
# docker push my-registry/my-etl:1.0

# 7. Déployer les manifests
kubectl apply -f manifests/etl-configmap.yaml
kubectl apply -f manifests/etl-secret.yaml
kubectl apply -f manifests/etl-cronjob.yaml

# 8. Tester manuellement
kubectl create job test-etl --from=cronjob/etl-daily -n data-pipeline

# 9. Voir les logs
kubectl logs -f job/test-etl -n data-pipeline

# 10. Vérifier dans PostgreSQL
kubectl exec -it postgres-postgresql-0 -n data-pipeline -- \
  psql -U de_user -d de_db -c "SELECT * FROM sales;"

📚 Ressources pour aller plus loin

🌐 Documentation officielle

📦 Charts Helm utiles

🔧 Outils


➡️ Prochaine étape

Maintenant que tu maîtrises les workloads data sur Kubernetes, passons au traitement de données haute performance avec Python !

👉 Module suivant : 17_polars_for_data_engineering — Polars : le DataFrame ultra-rapide

Tu vas apprendre : - Pourquoi Polars est plus rapide que Pandas - API lazy vs eager - Optimisations automatiques - Migration depuis Pandas


🎉 Félicitations ! Tu as terminé le module Kubernetes pour Workloads Data.

Retour au sommet