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-contextkubectl applyCrashLoopBackOff ?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 !
| 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é |
À la fin de ce module, tu seras capable de :
kubectl☸️ 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
| 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.
| 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 !
En Data Engineering moderne, tu dois souvent :
| 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 |
| 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 |
┌─────────────────────────────────────────────────────┐
│ CLUSTER K8s │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ NODE 1 │ │ NODE 2 │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Pod A │ │ │ │ Pod C │ │ │
│ │ └───────┘ │ │ └───────┘ │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Pod B │ │ │ │ Pod D │ │ │
│ │ └───────┘ │ │ └───────┘ │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
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 |
| 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é.
| Composant | Rôle |
|---|---|
| Kubelet | Agent qui gère les pods sur le node |
| Container Runtime | Docker/containerd qui exécute les containers |
┌─────────────────────────────────────────┐
│ CONTROL PLANE │
│ ┌────────────────┐ ┌───────────────┐ │
kubectl ─────────▶│ │ API Server │ │ Scheduler │ │
│ └───────┬────────┘ └───────────────┘ │
└──────────┼─────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ WORKER NODE 1 │ │ WORKER NODE 2 │ │ WORKER NODE 3 │
│ ┌───────────────┐ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ Kubelet │ │ │ │ Kubelet │ │ │ │ Kubelet │ │
│ └───────────────┘ │ │ └───────────────┘ │ │ └───────────────┘ │
│ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ │
│ │ Pod │ │ Pod │ │ │ │ Pod │ │ Pod │ │ │ │ Pod │ │
│ └─────┘ └─────┘ │ │ └─────┘ └─────┘ │ │ └─────┘ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
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é |
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
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
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) |
| 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).
┌──────────────┐
User ───────▶│ Service │
└──────┬───────┘
│
▼
┌──────────────┐
│ Deployment │
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ Pod 1 │ │ Pod 2 │ │ Pod 3 │
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
▼ ▼ ▼
Container Container Container
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 |
kubectl est l’outil CLI pour interagir avec Kubernetes.
Maintenant que Kubernetes est installé, apprenons à écrire les fichiers de configuration YAML.
YAML = “YAML Ain’t Markup Language”
YAML est un format de fichier pour écrire de la configuration de manière lisible par un humain.
| 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 ---
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.
| # | 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 |
# ══════════════════════════════════════════════════════════════
# 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 ~# ══════════════════════════════════════════════════════════════
# 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-
Avant d’écrire du YAML, il faut comprendre POURQUOI Kubernetes a différentes ressources.
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 |
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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-projetSans 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
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 → ✅
Tout manifest Kubernetes a cette structure :
| 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-resourceste donne la liste complète !
# 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# 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.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.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
⚠️ Rappel : Base64 n’est PAS du chiffrement ! C’est juste un encodage.
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# 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.yamlkubectl explainkubectl applyCHECKLIST 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
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Voici les commandes que tu utiliseras au quotidien :
| 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 |
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┌─────────────────────────────────────────────────────────────────┐
│ 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'│
└─────────────────────────────────────────────────────────────────┘
Déployons une application simple pour comprendre le cycle de déploiement.
# 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: NodePortMéthode 1 : Port-forward (recommandé)
Méthode 2 : NodePort
💡 Astuce entreprise : Le NodePort n’est pas toujours autorisé en entreprise (firewall).
port-forwardfonctionne partout !
kubectl apply Deployment créé ReplicaSet créé Pods créés
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌───────────┐ ┌─────────┐
│ YAML │ ───▶ │Deployment │ ───▶ │ReplicaSet │ ───▶ │ Pods │
└─────────┘ └───────────┘ └───────────┘ └─────────┘
│
Service expose les pods ◀──────────────────┘
Voici les ressources Kubernetes particulièrement utiles pour le Data Engineering.
Un Job exécute un ou plusieurs pods jusqu’à complétion : - ETL ponctuel - Migration de données - Backfill
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: 1Schedule 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 |
# 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.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CronJob │────▶│ Job │────▶│ Pod │
│ (0 2 * * *) │ │ │ │ (etl.py) │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌───────────────────────┤
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PVC │ │ PostgreSQL │
│ (input data) │ │ (output) │
└──────────────┘ └──────────────┘
Les données ne doivent jamais vivre uniquement dans un Pod (éphémère).
| 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…) |
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌──────────────────┐
│ Pod │────▶│ PVC │────▶│ PV │────▶│ Stockage réel │
│ │ │ (claim) │ │ (provision) │ │ (disk, NFS, EBS) │
└─────────┘ └─────────┘ └─────────────┘ └──────────────────┘
Access Modes :
| Mode | Description |
|---|---|
ReadWriteOnce |
Lecture/écriture par un seul node |
ReadOnlyMany |
Lecture seule par plusieurs nodes |
ReadWriteMany |
Lecture/écriture par plusieurs nodes |
Créer un secret en ligne de commande :
# 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| 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 |
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
| 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 |
| 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 |
| Ressource | Unité | Exemple |
|---|---|---|
| CPU | millicores | 500m = 0.5 CPU |
| Memory | Mi/Gi | 256Mi, 1Gi |
Réponds aux questions suivantes pour vérifier tes acquis.
✅ 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.
✅ Réponse : b — Les Pods sont éphémères. Sans PVC, les données sont perdues quand le Pod est supprimé ou recréé.
kubectl logs <pod>kubectl logs -p <pod>kubectl describe pod <pod>kubectl get logs <pod>✅ Réponse : b — L’option -p (previous) affiche les logs du container précédent, utile après un crash.
✅ 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.
✅ Réponse : c — Un CronJob permet de planifier des Jobs selon une expression cron (0 2 * * *).
CrashLoopBackOff ?✅ Réponse : c — K8s tente de redémarrer le container mais il plante à chaque fois. Utilisez kubectl logs <pod> pour voir l’erreur.
:latest:1.0.0✅ Réponse : b — Les tags versionnés garantissent la reproductibilité. :latest peut changer et causer des comportements inattendus.
Déployer un pipeline ETL complet sur Kubernetes : - PostgreSQL avec stockage persistant - CronJob Python qui charge des données CSV dans PostgreSQL
Tu dois créer une stack Data Engineering minimale mais réaliste, entièrement orchestrée par Kubernetes.
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
data-pipelinekubectl create job --from=cronjob/etl-daily test-etl1. manifests/namespace.yaml
2. manifests/postgres-secret.yaml
3. manifests/postgres-pvc.yaml
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-pvc5. manifests/postgres-service.yaml
6. manifests/etl-configmap.yaml
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: 18. 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;"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.