🟩 Niveau 2 : Intermédiaire
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_fundamentalsavant 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
bashVoir le code
# 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
bashVoir le code
# 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" ?
a) Une application web qui affiche des données
b) Une charge de travail dédiée au traitement, transformation ou déplacement de données
c) Un dashboard de visualisation
d) 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 ?
a) backoffLimit: 24
b) ttlSecondsAfterFinished: 86400
c) activeDeadlineSeconds: 86400
d) 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 ?
a) Deployment est plus récent
b) StatefulSet garantit une identité stable et un stockage persistant par pod
c) Deployment ne supporte pas les volumes
d) 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 ?
a) Un outil de monitoring
b) Un package manager pour Kubernetes
c) Un orchestrateur de containers
d) 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 ?
a) Le pod a été tué par l'administrateur
b) L'image Docker est corrompue
c) Le container a dépassé sa limite de mémoire
d) 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 ?
a) LocalExecutor
b) CeleryExecutor
c) KubernetesExecutor
d) 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 ?
a) BestEffort
b) Burstable
c) Guaranteed
d) 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
- Créer le namespace
data-pipeline - Déployer MinIO via Helm
- Déployer PostgreSQL via Helm
- Uploader les données dans MinIO
- Build & push l'image ETL
- Déployer le CronJob
- Tester manuellement
- 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
- Kubernetes Jobs — Documentation Jobs
- Kubernetes CronJobs — Documentation CronJobs
- StatefulSets — Applications stateful
- Helm Docs — Documentation Helm
📦 Charts Helm utiles
- Artifact Hub — Recherche de charts Helm
- Bitnami Charts — Charts de qualité production
🔧 Outils
- k9s — Terminal UI pour Kubernetes
- Lens — IDE Kubernetes
- MinIO Client (mc) — CLI pour MinIO
➡️ 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.