Kubernetes Fundamentals pour Data Engineers

Bienvenue dans ce module où tu vas apprendre à orchestrer des containers à grande échelle avec Kubernetes. Tu découvriras comment déployer, gérer et monitorer des applications data dans un cluster K8s — des compétences essentielles pour industrialiser tes pipelines !


Prérequis

Niveau Compétence
✅ Requis Avoir suivi le module 14_docker_for_data_engineers
✅ Requis Maîtriser les bases de Docker (images, containers, volumes)
✅ Requis Connaissances de base en YAML
💡 Recommandé Docker Desktop ou Minikube installé

🎯 Objectifs du module

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

  • Comprendre l’architecture et les concepts clés de Kubernetes
  • Installer et configurer un cluster Kubernetes local
  • Déployer et gérer des applications avec kubectl
  • Utiliser les Jobs et CronJobs pour des tâches Data Engineering
  • Configurer le stockage persistant (PVC)
  • Débugger des pods et diagnostiquer les erreurs courantes
  • Déployer et orchestrer un pipeline ETL complet dans Kubernetes

C’est quoi Kubernetes ?

☸️ Kubernetes (K8s) est une plateforme open-source d’orchestration de containers qui automatise le déploiement, la mise à l’échelle et la gestion d’applications containerisées.

En pratique, Kubernetes te permet de : - Déployer des containers sur plusieurs machines (un cluster) - Scaler automatiquement selon la charge - Redémarrer les containers qui plantent - Gérer la configuration et les secrets - Exposer tes applications au réseau

Kubernetes ≠ Docker (mais complémentaires)

Aspect Docker Kubernetes
Rôle Créer et exécuter des containers Orchestrer des containers
Échelle 1 machine Cluster de machines
Focus Build & Run Deploy, Scale, Manage
Analogie Le musicien Le chef d’orchestre

💡 Docker crée les containers, Kubernetes les orchestre à grande échelle.

Analogies pour bien comprendre

Analogie Explication
Chef d’orchestre K8s coordonne tous les containers (musiciens) pour qu’ils jouent en harmonie
Agence immobilière K8s place tes containers (locataires) dans les nodes (appartements) disponibles
Pilote automatique Tu définis la destination (état souhaité), K8s s’occupe d’y arriver et d’y rester
🏭 Usine automatisée Tu donnes les plans, K8s fabrique, surveille et remplace les pièces défectueuses

ℹ️ Le savais-tu ?

Le nom Kubernetes vient du grec κυβερνήτης (kubernḗtēs) qui signifie “pilote” ou “gouverneur” — celui qui tient la barre d’un navire.

K8s est né chez Google, inspiré de leur système interne Borg qui gère des milliards de containers depuis 2003. Google a ouvert le projet en 2014 et l’a donné à la Cloud Native Computing Foundation (CNCF).

Le “8” dans K8s représente les 8 lettres entre le K et le S de Kubernetes !

📖 Histoire de Kubernetes


1. Pourquoi Kubernetes pour un Data Engineer ?

En Data Engineering moderne, tu dois souvent :

  • Exécuter des jobs ETL de manière fiable et planifiée
  • Déployer des bases de données et des brokers (Kafka)
  • Faire tourner des jobs Spark distribués
  • Orchestrer avec Airflow ou Prefect
  • Scaler selon le volume de données

❌ Sans Kubernetes

Problème Conséquence
Déploiement manuel sur chaque serveur Lent et source d’erreurs
Pas de redémarrage automatique Jobs perdus en cas de crash
Scaling manuel Sous/sur-utilisation des ressources
Configuration dispersée Difficile à maintenir

✅ Avec Kubernetes

Avantage Exemple concret
Déploiement déclaratif kubectl apply -f etl-job.yaml
Auto-healing Pod crashé = recréé automatiquement
CronJobs natifs ETL planifié sans cron externe
Secrets management Credentials gérés proprement
Portabilité Même config en dev, staging, prod

Vue d’ensemble : Cluster K8s

┌─────────────────────────────────────────────────────┐
│                   CLUSTER K8s                       │
│                                                     │
│   ┌─────────────┐   ┌─────────────┐                │
│   │   NODE 1    │   │   NODE 2    │                │
│   │  ┌───────┐  │   │  ┌───────┐  │                │
│   │  │ Pod A │  │   │  │ Pod C │  │                │
│   │  └───────┘  │   │  └───────┘  │                │
│   │  ┌───────┐  │   │  ┌───────┐  │                │
│   │  │ Pod B │  │   │  │ Pod D │  │                │
│   │  └───────┘  │   │  └───────┘  │                │
│   └─────────────┘   └─────────────┘                │
│                                                     │
└─────────────────────────────────────────────────────┘

2. Architecture Kubernetes (simplifiée)

Un cluster Kubernetes est composé de deux types de machines :

Composant Rôle Analogie
Control Plane (Master) Cerveau du cluster, prend les décisions Le management
Worker Nodes Exécutent les containers Les ouvriers

Control Plane (les composants essentiels)

Composant Rôle
API Server Point d’entrée unique (kubectl → API)
Scheduler Décide sur quel node placer les pods

💡 Pour ce module “Fundamentals”, on se concentre sur ces 2 composants. Les autres (etcd, Controller Manager) seront vus en niveau avancé.

Worker Nodes

Composant Rôle
Kubelet Agent qui gère les pods sur le node
Container Runtime Docker/containerd qui exécute les containers

Schéma Architecture

                         ┌─────────────────────────────────────────┐
                         │           CONTROL PLANE                 │
                         │  ┌────────────────┐  ┌───────────────┐ │
       kubectl ─────────▶│  │   API Server   │  │   Scheduler   │ │
                         │  └───────┬────────┘  └───────────────┘ │
                         └──────────┼─────────────────────────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              │                     │                     │
              ▼                     ▼                     ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│     WORKER NODE 1   │ │     WORKER NODE 2   │ │     WORKER NODE 3   │
│  ┌───────────────┐  │ │  ┌───────────────┐  │ │  ┌───────────────┐  │
│  │    Kubelet    │  │ │  │    Kubelet    │  │ │  │    Kubelet    │  │
│  └───────────────┘  │ │  └───────────────┘  │ │  └───────────────┘  │
│  ┌─────┐  ┌─────┐   │ │  ┌─────┐  ┌─────┐   │ │  ┌─────┐           │
│  │ Pod │  │ Pod │   │ │  │ Pod │  │ Pod │   │ │  │ Pod │           │
│  └─────┘  └─────┘   │ │  └─────┘  └─────┘   │ │  └─────┘           │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘

3. Concepts clés Kubernetes

Voici les concepts fondamentaux à maîtriser :

Concept Description Analogie
Pod Plus petite unité déployable (1+ containers) Un appartement
Deployment Gère les replicas de pods, rolling updates Un syndic d’immeuble
Service Expose les pods sur le réseau L’adresse postale
Namespace Isolation logique des ressources Un quartier de la ville
ConfigMap Configuration externe (non sensible) Fichier .env public
Secret Données sensibles (base64) Un coffre-fort
PersistentVolumeClaim Demande de stockage persistant Location d’un disque dur
Job Tâche one-shot (s’exécute puis se termine) Une mission ponctuelle
CronJob Job planifié (comme cron Linux) Un réveil programmé

Pod

Le Pod est l’unité de base dans Kubernetes : - Contient 1 ou plusieurs containers qui partagent le réseau et le stockage - A sa propre adresse IP dans le cluster - Est éphémère : peut être détruit et recréé à tout moment

apiVersion: v1
kind: Pod
metadata:
  name: mon-pod
spec:
  containers:
  - name: mon-container
    image: python:3.11-slim

Deployment

Le Deployment gère un ensemble de pods identiques : - Garantit le nombre de replicas souhaité - Gère les rolling updates (mise à jour sans downtime) - Recrée les pods qui crashent

Service

Le Service expose les pods sur le réseau :

Type Description Usage
ClusterIP IP interne au cluster Communication entre pods
NodePort Port ouvert sur chaque node Tests, développement
LoadBalancer Load balancer cloud Production (AWS, GCP, Azure)

Secret vs ConfigMap

Aspect ConfigMap Secret
Usage Config non sensible Passwords, tokens, clés
Encodage Texte clair Base64
Exemple URLs, feature flags DB_PASSWORD, API_KEY

⚠️ Attention : Base64 n’est PAS du chiffrement !

Base64 est juste un encodage, pas une protection. N’importe qui avec accès au cluster peut décoder les secrets. En production, utilise Sealed Secrets, HashiCorp Vault, ou les secrets managers cloud (AWS Secrets Manager, GCP Secret Manager).

Schéma : Relations entre concepts

                    ┌──────────────┐
       User ───────▶│   Service    │
                    └──────┬───────┘
                           │
                           ▼
                    ┌──────────────┐
                    │  Deployment  │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
         ┌───────┐    ┌───────┐    ┌───────┐
         │ Pod 1 │    │ Pod 2 │    │ Pod 3 │
         └───┬───┘    └───┬───┘    └───┬───┘
             │            │            │
             ▼            ▼            ▼
        Container    Container    Container

4. Installation de Kubernetes

Pour ce module, tu as besoin d’un cluster Kubernetes local.

Option Difficulté Recommandé pour
Docker Desktop ⭐ Facile Mac/Windows, débutants
Minikube ⭐⭐ Moyen Tous OS, plus de contrôle
k3d/k3s ⭐⭐ Moyen Léger, CI/CD

Option 1 : Docker Desktop (recommandé)

  1. Ouvrir Docker Desktop
  2. Aller dans SettingsKubernetes
  3. Cocher Enable Kubernetes
  4. Cliquer Apply & Restart
  5. Attendre que le status passe au vert

Option 2 : Minikube

# Installation (macOS avec Homebrew)
brew install minikube

# Installation (Linux)
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

# Démarrer le cluster
minikube start

# Vérifier
minikube status

Installer kubectl

kubectl est l’outil CLI pour interagir avec Kubernetes.

# macOS
brew install kubectl

# Linux
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install kubectl /usr/local/bin/kubectl

# Windows (avec Docker Desktop, kubectl est inclus)

Vérification

# Version de kubectl
kubectl version --client

# Liste des nodes du cluster
kubectl get nodes

# Vérifier le contexte actif
kubectl config get-contexts
Voir le code
%%bash
# Vérifier l'installation de kubectl
echo "=== Version kubectl ==="
kubectl version --client

echo ""
echo "=== Nodes du cluster ==="
kubectl get nodes

echo ""
echo "=== Contexte actif ==="
kubectl config current-context

5. Maîtriser YAML pour Kubernetes

Maintenant que Kubernetes est installé, apprenons à écrire les fichiers de configuration YAML.

5.1 C’est quoi YAML et pourquoi Kubernetes l’utilise ?

YAML = “YAML Ain’t Markup Language”

YAML est un format de fichier pour écrire de la configuration de manière lisible par un humain.

Pourquoi YAML plutôt que JSON ou XML ?

Format Lisibilité Exemple
XML ❌ Verbeux <name>postgres</name>
JSON ⚠️ Moyen {"name": "postgres"}
YAML ✅ Simple name: postgres

Kubernetes a choisi YAML parce que : - Facile à lire : pas de {} ni de "" partout - Facile à écrire : structure claire avec l’indentation - Commentaires possibles : # ceci est un commentaire - Multi-documents : plusieurs ressources dans un seul fichier avec ---

Le principe fondamental de Kubernetes

Kubernetes fonctionne sur le principe déclaratif :

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   TOI (humain)          →        KUBERNETES (robot)         │
│                                                             │
│   "Je VEUX 3 pods       →    "OK, je CRÉE 3 pods et        │
│    avec nginx"                 je m'assure qu'il y en       │
│                                a TOUJOURS 3"                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Tu décris l’état souhaité dans un fichier YAML, Kubernetes s’occupe d’y arriver.


5.2 Les règles d’or du YAML

5 règles à ne JAMAIS oublier

# Règle ✅ Correct ❌ Incorrect
1 Indentation = 2 espaces name: test ⇥name: test (tab)
2 Espace après les : name: postgres name:postgres
3 Sensible à la casse apiVersion ApiVersion
4 Tiret - pour les listes - item item
5 Pas de tabs, jamais ! espaces uniquement tabs = erreur

Les types de données

# ══════════════════════════════════════════════════════════════
# STRINGS (texte)
# ══════════════════════════════════════════════════════════════
nom: postgres                    # Sans guillemets (recommandé)
nom: "postgres"                  # Avec guillemets (si caractères spéciaux)
message: "Bonjour: monde"        # Guillemets obligatoires si : dans la valeur

# ══════════════════════════════════════════════════════════════
# NOMBRES
# ══════════════════════════════════════════════════════════════
port: 5432                       # Entier
prix: 19.99                      # Décimal
replicas: 3                      # Entier

# ══════════════════════════════════════════════════════════════
# BOOLÉENS (vrai/faux)
# ══════════════════════════════════════════════════════════════
actif: true                      # ou false
debug: yes                       # ou no (équivalent)

# ══════════════════════════════════════════════════════════════
# NULL (vide)
# ══════════════════════════════════════════════════════════════
valeur: null                     # ou ~

Les structures de données

# ══════════════════════════════════════════════════════════════
# DICTIONNAIRE = ensemble de clé: valeur
# ══════════════════════════════════════════════════════════════
personne:                        # <- clé parente
  nom: Alice                     # <- 2 espaces = enfant de "personne"
  age: 30                        # <- même niveau que "nom"
  ville: Paris                   # <- même niveau

# ══════════════════════════════════════════════════════════════
# LISTE = ensemble d'éléments (avec tiret -)
# ══════════════════════════════════════════════════════════════
fruits:                          # <- clé parente
  - pomme                        # <- premier élément (tiret + espace)
  - banane                       # <- deuxième élément
  - orange                       # <- troisième élément

# ══════════════════════════════════════════════════════════════
# LISTE DE DICTIONNAIRES (très courant en K8s !)
# ══════════════════════════════════════════════════════════════
employes:
  - nom: Alice                   # <- premier employé
    poste: Dev                   #    propriété du premier employé
    age: 30                      #    propriété du premier employé
  - nom: Bob                     # <- deuxième employé (nouveau tiret)
    poste: DevOps                #    propriété du deuxième employé
    age: 25

💡 Astuce K8s : containers: est TOUJOURS une liste (même avec 1 seul container), donc toujours avec -


5.3 Pourquoi plusieurs types de ressources ?

Avant d’écrire du YAML, il faut comprendre POURQUOI Kubernetes a différentes ressources.

Le problème à résoudre

Imaginons que tu veux déployer une API Python :

Besoin Question Ressource K8s
Isoler mes ressources “Comment séparer dev/prod ?” Namespace
Exécuter mon code “Où tourne mon container ?” Pod
Avoir plusieurs instances “Et si un pod crashe ?” Deployment
Accéder depuis le réseau “Comment appeler mon API ?” Service
Stocker la config “Où mettre mes variables ?” ConfigMap
Stocker les secrets “Où mettre mes passwords ?” Secret
Planifier un job “Comment lancer mon ETL à 2h ?” CronJob

Schéma : Qui fait quoi ?

┌─────────────────────────────────────────────────────────────────────────┐
│                        POURQUOI CHAQUE RESSOURCE ?                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   👤 Utilisateur                                                        │
│        │                                                                │
│        │ "Je veux accéder à mon-api sur le port 80"                    │
│        ▼                                                                │
│   ┌─────────────────┐                                                  │
│   │    SERVICE      │  ← Donne une adresse stable (DNS + IP)           │
│   │   "mon-api"     │  ← Load balance entre les pods                   │
│   │    port: 80     │                                                  │
│   └────────┬────────┘                                                  │
│            │                                                            │
│            │ "Envoie le trafic aux pods avec label app=mon-api"        │
│            ▼                                                            │
│   ┌─────────────────┐                                                  │
│   │   DEPLOYMENT    │  ← Garantit 3 replicas                           │
│   │   replicas: 3   │  ← Recrée les pods qui crashent                  │
│   │                 │  ← Gère les mises à jour (rolling update)        │
│   └────────┬────────┘                                                  │
│            │                                                            │
│            │ "Crée et maintient 3 pods identiques"                     │
│            ▼                                                            │
│   ┌─────────┐ ┌─────────┐ ┌─────────┐                                 │
│   │  POD 1  │ │  POD 2  │ │  POD 3  │  ← Chaque pod a son IP          │
│   │ (nginx) │ │ (nginx) │ │ (nginx) │  ← Chaque pod peut mourir       │
│   └─────────┘ └─────────┘ └─────────┘                                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Pourquoi un Namespace ?

Un Namespace est un espace isolé dans le cluster, comme des dossiers sur ton ordinateur.

┌─────────────────────────────────────────────────────────────────────────┐
│                            CLUSTER K8S                                  │
│                                                                         │
│   ┌─────────────────────┐    ┌─────────────────────┐                   │
│   │  NAMESPACE: dev     │    │  NAMESPACE: prod    │                   │
│   │  ┌───────┐ ┌───────┐│    │  ┌───────┐ ┌───────┐│                   │
│   │  │mon-api│ │  db   ││    │  │mon-api│ │  db   ││                   │
│   │  └───────┘ └───────┘│    │  └───────┘ └───────┘│                   │
│   └─────────────────────┘    └─────────────────────┘                   │
│                                                                         │
│   ┌─────────────────────┐    ┌─────────────────────┐                   │
│   │ NAMESPACE: staging  │    │ NAMESPACE: data-eng │                   │
│   │  ┌───────┐          │    │  ┌───────┐ ┌───────┐│                   │
│   │  │mon-api│          │    │  │airflow│ │ spark ││                   │
│   │  └───────┘          │    │  └───────┘ └───────┘│                   │
│   └─────────────────────┘    └─────────────────────┘                   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Pourquoi utiliser des Namespaces ?

Avantage Exemple
Isolation Les ressources de dev ne voient pas celles de prod
Organisation Séparer par équipe, par environnement, par projet
Quotas Limiter les ressources CPU/RAM par namespace
Permissions Donner accès à dev mais pas à prod

Namespaces par défaut :

Namespace Usage
default Où vont les ressources si tu ne précises pas
kube-system Composants internes de K8s (ne pas toucher !)
kube-public Ressources accessibles à tous
# Lister les namespaces
kubectl get namespaces

# Créer un namespace
kubectl create namespace mon-projet

# Travailler dans un namespace spécifique
kubectl get pods -n mon-projet
kubectl apply -f manifest.yaml -n mon-projet

# Changer le namespace par défaut
kubectl config set-context --current --namespace=mon-projet

Pourquoi un Service EST indispensable ?

Sans Service :

Pod 1 : IP = 10.1.0.15  ← Cette IP change à chaque recréation du pod !
Pod 2 : IP = 10.1.0.23  ← Comment savoir quelle IP appeler ?
Pod 3 : IP = 10.1.0.41  ← Impossible de hardcoder ces IPs

Avec Service :

Service "mon-api" : IP stable = 10.0.0.100 (ne change jamais)
                    DNS = mon-api.default.svc.cluster.local
                    → Redirige automatiquement vers les pods vivants

Pourquoi un Deployment et pas juste des Pods ?

Sans Deployment (pods manuels) :

- Pod crashe → Personne ne le recrée → Service DOWN 
- Mise à jour → Supprimer/recréer manuellement → Downtime 
- Scaling → Créer chaque pod à la main → Lent 

Avec Deployment :

- Pod crashe → Deployment le recrée automatiquement → ✅
- Mise à jour → Rolling update sans downtime → ✅
- Scaling → kubectl scale deployment mon-api --replicas=10 → ✅

5.4 Structure d’un manifest YAML

La structure de base (4 champs obligatoires)

Tout manifest Kubernetes a cette structure :

apiVersion: ???     # 1. Quelle version de l'API ?
kind: ???           # 2. Quel type de ressource ?
metadata:           # 3. Comment s'appelle-t-elle ?
  name: ???
spec:               # 4. Qu'est-ce que tu veux ?
  ???

Trouver apiVersion et kind

Je veux créer… kind apiVersion
Un espace isolé Namespace v1
Un container simple Pod v1
Plusieurs replicas d’un pod Deployment apps/v1
Un point d’accès réseau Service v1
De la configuration ConfigMap v1
Des données secrètes Secret v1
Un job one-shot Job batch/v1
Un job planifié CronJob batch/v1

💡 Astuce : kubectl api-resources te donne la liste complète !


5.5 Écrire chaque ressource (ligne par ligne)

Namespace (créer un espace isolé)

# namespace.yaml
# ─────────────────────────────────────────────────────────────
apiVersion: v1                    # API de base
kind: Namespace                   # Type = Namespace
# ─────────────────────────────────────────────────────────────
metadata:
  name: data-pipeline             # Nom du namespace
  labels:                         # Labels optionnels
    team: data-engineering
    env: production
kubectl apply -f namespace.yaml
kubectl get namespaces

Pod (le plus simple)

# pod.yaml
# ─────────────────────────────────────────────────────────────
apiVersion: v1                    # Pods utilisent l'API v1
kind: Pod                         # Je veux créer un Pod
# ─────────────────────────────────────────────────────────────
metadata:
  name: mon-premier-pod           # Nom du pod (unique dans le namespace)
  namespace: data-pipeline        # Dans quel namespace ?
  labels:                         # Labels = tags pour organiser
    app: demo                     # Label arbitraire (clé: valeur)
# ─────────────────────────────────────────────────────────────
spec:
  containers:                     # Liste des containers (avec tiret !)
  - name: nginx                   # Nom du container
    image: nginx:alpine           # Image Docker à utiliser
    ports:
    - containerPort: 80           # Port exposé par le container

Deployment (le standard en production)

# deployment.yaml
# ─────────────────────────────────────────────────────────────
apiVersion: apps/v1               # Deployments utilisent apps/v1
kind: Deployment                  # Type = Deployment
# ─────────────────────────────────────────────────────────────
metadata:
  name: mon-api                   # Nom du deployment
  namespace: data-pipeline        # Dans quel namespace ?
# ─────────────────────────────────────────────────────────────
spec:
  replicas: 3                     # Je veux 3 pods identiques
  
  selector:                       # Comment identifier MES pods ?
    matchLabels:
      app: mon-api                # Cherche les pods avec ce label
  
  template:                       # Modèle pour créer les pods
    metadata:
      labels:
        app: mon-api              # ⚠️ DOIT correspondre au selector !
    spec:
      containers:
      - name: api
        image: nginx:alpine
        ports:
        - containerPort: 80
        resources:                # Limites de ressources
          requests:               # Minimum garanti
            memory: "64Mi"
            cpu: "50m"
          limits:                 # Maximum autorisé
            memory: "128Mi"
            cpu: "100m"

⚠️ La règle CRITIQUE du selector :

spec.selector.matchLabels.app  ═══════╗
                                      ║ DOIVENT ÊTRE IDENTIQUES !
spec.template.metadata.labels.app ════╝

Si différents → Erreur : "selector does not match template labels"

Service (exposer sur le réseau)

# service.yaml
# ─────────────────────────────────────────────────────────────
apiVersion: v1                    # Services utilisent l'API v1
kind: Service                     # Type = Service
# ─────────────────────────────────────────────────────────────
metadata:
  name: mon-api-svc               # Nom = nom DNS dans le cluster
  namespace: data-pipeline
# ─────────────────────────────────────────────────────────────
spec:
  selector:
    app: mon-api                  # Cible les pods avec ce label
  
  type: ClusterIP                 # ClusterIP | NodePort | LoadBalancer
  
  ports:
  - port: 80                      # Port du SERVICE (ce qu'on appelle)
    targetPort: 80                # Port du CONTAINER (où ça arrive)

Mapping des ports :

Client → Service:80 → Pod:80 → Container

curl http://mon-api-svc:80  →  redirigé vers  →  container:80

ConfigMap (configuration non sensible)

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: data-pipeline
data:                            # Clé: valeur (toujours strings)
  LOG_LEVEL: "info"
  DB_HOST: "postgres.data-pipeline.svc.cluster.local"
  DB_PORT: "5432"

Secret (données sensibles)

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: data-pipeline
type: Opaque
stringData:                      # K8s encode automatiquement en base64
  DB_PASSWORD: "SuperSecret123"
  API_KEY: "sk-xxxx-yyyy-zzzz"

⚠️ Rappel : Base64 n’est PAS du chiffrement ! C’est juste un encodage.


5.6 Exemple complet : Déployer une app de A à Z

Voici comment déployer une application complète :

# 1. Créer le namespace
kubectl create namespace demo-app

# 2. Appliquer tous les manifests
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f secret.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

# OU tout d'un coup (si dans un dossier)
kubectl apply -f ./manifests/

# 3. Vérifier
kubectl get all -n demo-app

# 4. Tester l'accès
kubectl port-forward svc/mon-api-svc 8080:80 -n demo-app
# Ouvrir http://localhost:8080

5.7 Outils pour t’aider

Générer un template automatiquement

# Ne jamais écrire from scratch ! Génère un template :
kubectl create namespace mon-ns --dry-run=client -o yaml > namespace.yaml
kubectl create deployment nginx --image=nginx --dry-run=client -o yaml > deployment.yaml
kubectl create service clusterip nginx --tcp=80:80 --dry-run=client -o yaml > service.yaml

Valider avant d’appliquer

# Vérifier la syntaxe sans créer
kubectl apply -f manifest.yaml --dry-run=client

# Valider côté serveur (plus strict)
kubectl apply -f manifest.yaml --dry-run=server

Explorer avec kubectl explain

kubectl explain pod
kubectl explain pod.spec.containers
kubectl explain deployment.spec.template.spec

5.8 Checklist avant kubectl apply

CHECKLIST YAML KUBERNETES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[ ] Indentation = 2 ESPACES (pas de tabs !)
[ ] Espace après chaque ":"
[ ] apiVersion correct pour le kind
[ ] metadata.name présent et unique
[ ] namespace spécifié (si pas default)
[ ] Pour Deployment : selector.matchLabels = template.labels
[ ] containers: avec un tiret (c'est une liste !)
[ ] Validé avec --dry-run=client
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

6. Commandes kubectl essentielles

Voici les commandes que tu utiliseras au quotidien :

CRUD sur les ressources

Action Commande
Créer/Mettre à jour kubectl apply -f manifest.yaml
Lire kubectl get pods
Détails kubectl describe pod <name>
Supprimer kubectl delete -f manifest.yaml

Lister les ressources

kubectl get pods                    # Lister les pods
kubectl get pods -o wide            # Avec plus de détails (node, IP)
kubectl get deployments             # Lister les deployments
kubectl get services                # Lister les services
kubectl get all                     # Tout lister
kubectl get all -n <namespace>      # Dans un namespace spécifique

Debug

kubectl logs <pod>                  # Voir les logs
kubectl logs -f <pod>               # Suivre les logs en temps réel
kubectl logs -p <pod>               # Logs du container précédent (après crash)
kubectl describe pod <pod>          # Détails + Events
kubectl exec -it <pod> -- bash      # Shell dans le pod
kubectl get events                  # Tous les events du namespace

Réseau

kubectl port-forward <pod> 8080:80           # Forward un port
kubectl port-forward svc/<service> 8080:80   # Forward depuis un service

Namespaces

kubectl get namespaces              # Lister les namespaces
kubectl create namespace dev        # Créer un namespace
kubectl config set-context --current --namespace=dev  # Changer de namespace par défaut

Cheat Sheet visuel

┌─────────────────────────────────────────────────────────────────┐
│                    KUBECTL CHEAT SHEET                         │
├─────────────────────────────────────────────────────────────────┤
│  CREATE/UPDATE │  kubectl apply -f manifest.yaml               │
│  READ          │  kubectl get pods/deploy/svc/all              │
│  DESCRIBE      │  kubectl describe <resource> <name>           │
│  DELETE        │  kubectl delete -f manifest.yaml              │
├─────────────────────────────────────────────────────────────────┤
│  LOGS          │  kubectl logs -f <pod>                        │
│  SHELL         │  kubectl exec -it <pod> -- bash               │
│  PORT-FORWARD  │  kubectl port-forward <pod> 8080:80           │
│  EVENTS        │  kubectl get events --sort-by='.lastTimestamp'│
└─────────────────────────────────────────────────────────────────┘
Voir le code
%%bash
# Exemples de commandes kubectl

echo "=== Namespaces ==="
kubectl get namespaces

echo ""
echo "=== Pods dans tous les namespaces ==="
kubectl get pods --all-namespaces

echo ""
echo "=== Services ==="
kubectl get services

7. Premier déploiement : Hello World

Déployons une application simple pour comprendre le cycle de déploiement.

Manifest YAML : Deployment + Service

# hello-world.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world
  labels:
    app: hello-world
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hello-world-svc
spec:
  selector:
    app: hello-world
  ports:
  - port: 80
    targetPort: 80
  type: NodePort

Déployer

# Appliquer le manifest
kubectl apply -f hello-world.yaml

# Vérifier le déploiement
kubectl get deployments
kubectl get pods
kubectl get services

Accéder à l’application

Méthode 1 : Port-forward (recommandé)

kubectl port-forward svc/hello-world-svc 8080:80
# Ouvrir http://localhost:8080

Méthode 2 : NodePort

# Trouver le NodePort
kubectl get svc hello-world-svc
# Accéder via http://localhost:<NodePort>

💡 Astuce entreprise : Le NodePort n’est pas toujours autorisé en entreprise (firewall). port-forward fonctionne partout !

Cycle de déploiement

kubectl apply     Deployment créé     ReplicaSet créé     Pods créés
     │                  │                   │                  │
     ▼                  ▼                   ▼                  ▼
┌─────────┐       ┌───────────┐       ┌───────────┐      ┌─────────┐
│  YAML   │ ───▶  │Deployment │ ───▶  │ReplicaSet │ ───▶ │  Pods   │
└─────────┘       └───────────┘       └───────────┘      └─────────┘
                                                               │
                    Service expose les pods ◀──────────────────┘

Nettoyage

kubectl delete -f hello-world.yaml

8. Kubernetes pour Data Engineering

Voici les ressources Kubernetes particulièrement utiles pour le Data Engineering.

7.1 Job : tâche one-shot

Un Job exécute un ou plusieurs pods jusqu’à complétion : - ETL ponctuel - Migration de données - Backfill

# etl-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: etl-job
spec:
  template:
    spec:
      containers:
      - name: etl
        image: python:3.11-slim
        command: ["python", "-c", "print('ETL terminé !')"]
      restartPolicy: Never
  backoffLimit: 3  # Nombre de retries en cas d'échec
# Lancer le job
kubectl apply -f etl-job.yaml

# Voir le status
kubectl get jobs

# Voir les logs
kubectl logs job/etl-job

7.2 CronJob : tâche planifiée

Un CronJob lance des Jobs selon un schedule (comme cron Linux) : - ETL quotidien - Nettoyage de données - Rapports automatisés

# etl-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: etl-daily
spec:
  schedule: "0 2 * * *"  # Tous les jours à 2h du matin
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: etl
            image: my-etl-image:1.0
            env:
            - name: DB_HOST
              value: "postgres"
          restartPolicy: OnFailure
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

Schedule cron :

Expression Signification
0 2 * * * Tous les jours à 2h00
*/15 * * * * Toutes les 15 minutes
0 0 * * 0 Tous les dimanches à minuit
0 8 1 * * Le 1er de chaque mois à 8h
# Lister les cronjobs
kubectl get cronjobs

# Déclencher manuellement
kubectl create job --from=cronjob/etl-daily test-etl

7.3 Déployer PostgreSQL dans K8s

# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_USER
          value: "de_user"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        - name: POSTGRES_DB
          value: "de_db"
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

⚠️ Note production : Les bases de données dans K8s nécessitent toujours un stockage persistant (PVC) (A voir plus en bas). En production, on préfère souvent des services managés (Cloud SQL, RDS, Azure Database) pour la haute disponibilité et les backups automatiques.

Schéma Pipeline Data Engineering sur K8s

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   CronJob    │────▶│     Job      │────▶│     Pod      │
│  (0 2 * * *) │     │              │     │  (etl.py)    │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │
                          ┌───────────────────────┤
                          │                       │
                          ▼                       ▼
                   ┌──────────────┐       ┌──────────────┐
                   │     PVC      │       │  PostgreSQL  │
                   │ (input data) │       │   (output)   │
                   └──────────────┘       └──────────────┘

9. Stockage : Volumes & PVC

Les données ne doivent jamais vivre uniquement dans un Pod (éphémère).

Concepts clés

Concept Description
PersistentVolume (PV) Ressource de stockage provisionnée par l’admin
PersistentVolumeClaim (PVC) Demande de stockage par l’utilisateur
StorageClass Type de stockage (SSD, HDD, cloud…)

Schéma : Comment ça marche

┌─────────┐     ┌─────────┐     ┌─────────────┐     ┌──────────────────┐
│   Pod   │────▶│   PVC   │────▶│     PV      │────▶│  Stockage réel   │
│         │     │ (claim) │     │ (provision) │     │ (disk, NFS, EBS) │
└─────────┘     └─────────┘     └─────────────┘     └──────────────────┘

Exemple : PVC pour PostgreSQL

# postgres-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

Access Modes :

Mode Description
ReadWriteOnce Lecture/écriture par un seul node
ReadOnlyMany Lecture seule par plusieurs nodes
ReadWriteMany Lecture/écriture par plusieurs nodes

Utilisation dans un Deployment

spec:
  containers:
  - name: postgres
    volumeMounts:
    - name: postgres-storage
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: postgres-storage
    persistentVolumeClaim:
      claimName: postgres-pvc
# Vérifier les PVC
kubectl get pvc

# Vérifier les PV
kubectl get pv

9. Configuration : ConfigMaps & Secrets

ConfigMap : configuration non sensible

# config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: etl-config
data:
  DB_HOST: "postgres"
  DB_PORT: "5432"
  LOG_LEVEL: "INFO"

Secret : données sensibles

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
data:
  password: ZGVfcGFzc3dvcmQ=  # base64 de "de_password"

Créer un secret en ligne de commande :

# Créer un secret
kubectl create secret generic postgres-secret \
  --from-literal=password=de_password

# Encoder en base64
echo -n "de_password" | base64

# Décoder
echo "ZGVfcGFzc3dvcmQ=" | base64 -d

Utilisation dans un Pod

spec:
  containers:
  - name: etl
    env:
    # Depuis ConfigMap
    - name: DB_HOST
      valueFrom:
        configMapKeyRef:
          name: etl-config
          key: DB_HOST
    # Depuis Secret
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: postgres-secret
          key: password

10. Debug & Monitoring

Commandes de debug essentielles

# Logs
kubectl logs <pod>                  # Logs actuels
kubectl logs -f <pod>               # Suivre en temps réel
kubectl logs -p <pod>               # Logs du container précédent (après crash)
kubectl logs <pod> -c <container>   # Logs d'un container spécifique

# Détails et Events
kubectl describe pod <pod>          # Détails complets + Events
kubectl get events --sort-by='.lastTimestamp'  # Events triés par date

# Shell dans le pod
kubectl exec -it <pod> -- bash      # Ouvrir un shell
kubectl exec -it <pod> -- sh        # Pour images Alpine

# Ressources
kubectl top pods                    # CPU/RAM des pods (si metrics-server)
kubectl top nodes                   # CPU/RAM des nodes

Erreurs classiques et solutions

Erreur Cause probable Solution
CrashLoopBackOff L’app plante au démarrage kubectl logs <pod> pour voir l’erreur
ImagePullBackOff Image introuvable ou accès refusé Vérifier nom/tag, credentials registry
Pending Pas de ressources disponibles kubectl describe pod → voir Events
OOMKilled Mémoire insuffisante Augmenter resources.limits.memory
CreateContainerConfigError ConfigMap/Secret manquant Vérifier que les refs existent
ErrImageNeverPull Image locale introuvable imagePullPolicy: IfNotPresent

Processus de debug

1. kubectl get pods              → Voir le status
           │
           ▼
2. kubectl describe pod <pod>    → Voir les Events
           │
           ▼
3. kubectl logs <pod>            → Voir les logs de l'app
           │
           ▼
4. kubectl exec -it <pod> -- sh  → Investiguer dans le container

11. Erreurs fréquentes & Bonnes pratiques

❌ Erreurs fréquentes des débutants

Erreur Conséquence
Pas de resources.requests/limits Pods non schedulés ou OOMKilled
Secrets dans ConfigMap Fuite de credentials
Base de données sans PVC Perte de données au restart
Tout dans namespace default Difficile à gérer, pas d’isolation
Images en :latest Comportement non reproductible
Pas de labels Impossible de filtrer/organiser

✅ Bonnes pratiques

Pratique Pourquoi
Toujours définir requests/limits Scheduling prévisible, protection contre OOM
Un namespace par projet/env Isolation, quotas, RBAC
Tags d’image versionnés image:1.0.0 au lieu de :latest
Labels systématiques Organisation et filtrage
Healthchecks (probes) K8s sait si l’app est prête/vivante
PVC pour toute donnée importante Persistance garantie

Exemple de labels recommandés

metadata:
  name: etl-job
  labels:
    app: etl-pipeline
    component: etl
    env: dev
    team: data-engineering
    version: "1.0.0"

Exemple de resources requests/limits

spec:
  containers:
  - name: etl
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "512Mi"
        cpu: "500m"
Ressource Unité Exemple
CPU millicores 500m = 0.5 CPU
Memory Mi/Gi 256Mi, 1Gi

Quiz de fin de module

Réponds aux questions suivantes pour vérifier tes acquis.


❓ Q1. Quelle est la différence entre un Pod et un Deployment ?

  1. Un Pod contient plusieurs Deployments
  2. Un Deployment gère des replicas de Pods et leur cycle de vie
  3. C’est la même chose
  4. Un Pod est pour la production, un Deployment pour le dev
💡 Voir la réponse

Réponse : b — Un Pod est l’unité de base (éphémère), un Deployment gère plusieurs replicas de Pods, les rolling updates, et recrée les Pods qui crashent.


❓ Q2. Pourquoi utiliser un PersistentVolumeClaim (PVC) ?

  1. Pour augmenter la vitesse du CPU
  2. Pour persister les données même si le Pod est supprimé
  3. Pour exposer un Pod sur internet
  4. Pour chiffrer les secrets
💡 Voir la réponse

Réponse : b — Les Pods sont éphémères. Sans PVC, les données sont perdues quand le Pod est supprimé ou recréé.


❓ Q3. Quelle commande permet de voir les logs d’un Pod qui a crashé ?

  1. kubectl logs <pod>
  2. kubectl logs -p <pod>
  3. kubectl describe pod <pod>
  4. kubectl get logs <pod>
💡 Voir la réponse

Réponse : b — L’option -p (previous) affiche les logs du container précédent, utile après un crash.


❓ Q4. L’encodage Base64 des Secrets Kubernetes est-il sécurisé ?

  1. Oui, c’est du chiffrement fort
  2. Non, Base64 est juste un encodage, pas du chiffrement
  3. Oui, si on utilise une clé de 256 bits
  4. Ça dépend de la version de Kubernetes
💡 Voir la réponse

Réponse : b — Base64 est un simple encodage réversible. N’importe qui avec accès au cluster peut décoder les secrets. En production, utilisez Sealed Secrets ou un secrets manager.


❓ Q5. Quelle ressource utiliser pour exécuter un ETL tous les jours à 2h du matin ?

  1. Deployment
  2. Job
  3. CronJob
  4. DaemonSet
💡 Voir la réponse

Réponse : c — Un CronJob permet de planifier des Jobs selon une expression cron (0 2 * * *).


❓ Q6. Que signifie l’erreur CrashLoopBackOff ?

  1. L’image Docker n’existe pas
  2. Le Pod manque de mémoire
  3. L’application dans le container plante au démarrage de manière répétée
  4. Le réseau est inaccessible
💡 Voir la réponse

Réponse : c — K8s tente de redémarrer le container mais il plante à chaque fois. Utilisez kubectl logs <pod> pour voir l’erreur.


❓ Q7. Quelle est la meilleure pratique pour les tags d’images en production ?

  1. Toujours utiliser :latest
  2. Utiliser des tags versionnés comme :1.0.0
  3. Ne pas mettre de tag
  4. Utiliser la date comme tag
💡 Voir la réponse

Réponse : b — Les tags versionnés garantissent la reproductibilité. :latest peut changer et causer des comportements inattendus.


Mini-projet : Pipeline ETL sur Kubernetes

Objectif

Déployer un pipeline ETL complet sur Kubernetes : - PostgreSQL avec stockage persistant - CronJob Python qui charge des données CSV dans PostgreSQL

Contexte

Tu dois créer une stack Data Engineering minimale mais réaliste, entièrement orchestrée par Kubernetes.

Structure du projet

k8s-etl-project/
├── manifests/
│   ├── namespace.yaml
│   ├── postgres-secret.yaml
│   ├── postgres-pvc.yaml
│   ├── postgres-deployment.yaml
│   ├── postgres-service.yaml
│   ├── etl-configmap.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 PostgreSQL (Secret + PVC + Deployment + Service)
  3. Build & push l’image Docker de l’ETL
  4. Déployer le CronJob ETL
  5. Tester manuellement : kubectl create job --from=cronjob/etl-daily test-etl
  6. 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

2. manifests/postgres-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: data-pipeline
type: Opaque
data:
  password: ZGVfcGFzc3dvcmQ=  # de_password

3. manifests/postgres-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: data-pipeline
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

4. manifests/postgres-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: data-pipeline
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_USER
          value: "de_user"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        - name: POSTGRES_DB
          value: "de_db"
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

5. manifests/postgres-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: data-pipeline
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
  type: ClusterIP

6. manifests/etl-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: etl-config
  namespace: data-pipeline
data:
  DB_HOST: "postgres"
  DB_PORT: "5432"
  DB_NAME: "de_db"
  DB_USER: "de_user"

7. manifests/etl-cronjob.yaml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: etl-daily
  namespace: data-pipeline
  labels:
    app: etl-pipeline
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app: etl-job
        spec:
          containers:
          - name: etl
            image: my-etl-image:1.0  # À remplacer par ton image
            envFrom:
            - configMapRef:
                name: etl-config
            env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            resources:
              requests:
                memory: "128Mi"
                cpu: "100m"
              limits:
                memory: "256Mi"
                cpu: "200m"
          restartPolicy: OnFailure
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

8. etl/etl.py

import os
import pandas as pd
from sqlalchemy import create_engine

def main():
    # Config depuis variables d'environnement
    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']
    
    print("🚀 Démarrage ETL...")
    
    # Simuler des données (en vrai: lire depuis PVC ou S3)
    df = pd.DataFrame({
        'date': ['2024-01-01', '2024-01-02'],
        'product': ['Laptop', 'Mouse'],
        'quantity': [5, 20],
        'price': [999.99, 29.99]
    })
    df['total'] = df['quantity'] * df['price']
    
    # Charger 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é !")

if __name__ == "__main__":
    main()

9. Commandes de déploiement

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

# Déployer PostgreSQL
kubectl apply -f manifests/postgres-secret.yaml
kubectl apply -f manifests/postgres-pvc.yaml
kubectl apply -f manifests/postgres-deployment.yaml
kubectl apply -f manifests/postgres-service.yaml

# Vérifier que PostgreSQL est prêt
kubectl get pods -n data-pipeline

# Déployer le CronJob
kubectl apply -f manifests/etl-configmap.yaml
kubectl apply -f manifests/etl-cronjob.yaml

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

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

# Vérifier dans PostgreSQL
kubectl exec -it $(kubectl get pod -l app=postgres -n data-pipeline -o name) \
  -n data-pipeline -- psql -U de_user -d de_db -c "SELECT * FROM sales;"

📚 Ressources pour aller plus loin

🌐 Documentation officielle

🎮 Pratique

📖 Livres & Cours

  • Kubernetes Up & Running — Kelsey Hightower
  • The Kubernetes Book — Nigel Poulton

🔧 Outils utiles

  • k9s — Terminal UI pour Kubernetes
  • Lens — IDE Kubernetes
  • kubectx/kubens — Changer de contexte/namespace facilement

➡️ Prochaine étape

Maintenant que tu maîtrises les fondamentaux de Kubernetes, passons aux workloads Data Engineering avancés !

👉 Module suivant : 16_k8s_for_data_workloads — Kubernetes pour les workloads Data

Tu vas apprendre : - Spark on Kubernetes - Airflow on Kubernetes - Helm charts pour packager tes déploiements - Horizontal Pod Autoscaler


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

Retour au sommet