Partie 4 : La grande migration vers Kubernetes (K3s)

Partie 4 : La grande migration vers Kubernetes (K3s)

Novembre 2025

Le déclic : Pourquoi Kubernetes ?

Mon site Flask tournait bien dans Docker, mais je voyais plus grand :

  • Mon NAS hébergeait une dizaine de services Docker
  • 2 Raspberry Pi prenaient la poussière
  • Pas de haute disponibilité : Si le NAS tombe, tout tombe
  • Pas d'orchestration : Gestion manuelle des containers

Préparation du cluster K3s

Pour un guide complet et détaillé de l'installation K3s (préparation réseau, configuration système, concepts Kubernetes, etc.), consultez mon article technique : Installation d'un cluster K3s sur Raspberry Pi

Le matériel final

Hostname IP Rôle Matériel RAM
rpi4-master 192.168.1.51 Control Plane RPi 4 8GB
rpi4-worker 192.168.1.50 Worker RPi 4 2GB
rpi3-worker1 192.168.1.36 Worker RPi 3 1GB
rpi3-worker2 192.168.1.15 Worker RPi 3 1GB

Installation de K3s (résumé)

# Sur le master
curl -sfL https://get.k3s.io | sh -s - \
  --disable traefik \
  --write-kubeconfig-mode 644

# Sur chaque worker
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.51:6443 \
  K3S_TOKEN=<TOKEN> sh -

# Vérification
kubectl get nodes
NAME           STATUS   ROLES                  AGE
rpi4-master    Ready    control-plane,master   5m
rpi4-worker    Ready    <none>                 2m
rpi3-worker1   Ready    <none>                 1m
rpi3-worker2   Ready    <none>                 1m

Migration de Docker vers K3s

1. Création du namespace

kubectl create namespace home-fonta

2. Build de l'image pour ARM64

Premier piège : Mon PC est en x86, les RPi en ARM64 !

# Tentative avec buildx (échec, trop lent)
docker buildx build --platform linux/arm64 ...

# Solution : Build directement sur le Pi !
ssh pi@192.168.1.51
git clone https://github.com/mon-repo/home-fonta.git
docker build -t nocoblas/home-fonta-web:v2.0 .
docker push nocoblas/home-fonta-web:v2.0

3. Premier deployment Kubernetes

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: home-fonta-web
  namespace: home-fonta
spec:
  replicas: 3
  selector:
    matchLabels:
      app: homefonta
  template:
    metadata:
      labels:
        app: homefonta
    spec:
      containers:
      - name: web
        image: nocoblas/home-fonta-web:v2.0
        ports:
        - containerPort: 8000
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

4. Service ClusterIP

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: home-fonta-web
  namespace: home-fonta
spec:
  selector:
    app: homefonta
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP

5. Ingress avec nginx-ingress

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: home-fonta-web
  namespace: home-fonta
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: home-fonta.fr
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: home-fonta-web
            port:
              number: 80

Premier problème : CrashLoopBackOff

kubectl get pods -n home-fonta
NAME                              READY   STATUS             RESTARTS
home-fonta-web-59c5486fb5-nl22t   0/1     CrashLoopBackOff   19

kubectl logs -n home-fonta home-fonta-web-59c5486fb5-nl22t
# Erreur : Port 8000 already in use

Cause : Les health probes pointaient vers le mauvais port !

Solution :

livenessProbe:
  httpGet:
    path: /health
    port: 8000  # Port de Gunicorn, pas 80 !

Problème majeur : Le réseau CNI

Le pire problème rencontré : Les pods sur différents nodes ne communiquaient pas !

# Ingress sur master → Pod sur worker = TIMEOUT
# Ingress sur master → Pod sur master = OK

# Test de connectivité
kubectl exec -it debug-pod -- ping 10.42.1.41
# Timeout !

Investigation du problème CNI

# Vérification Flannel
kubectl get pods -n kube-flannel
# Tous Running... mais ça ne marche pas

# Logs
kubectl logs -n kube-flannel kube-flannel-ds-xxx
# Erreur iptables

Solution temporaire : NodeSelector

# Force tous les pods sur le master
nodeSelector:
  kubernetes.io/hostname: rpi4-master

Pas idéal, mais ça marche ! Investigation du CNI remise à plus tard.

Ajout des certificats SSL avec cert-manager

# Installation cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

# ClusterIssuer pour Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: mon-email@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-dns
    solvers:
    - dns01:
        cloudflare:
          email: mon-email@example.com
          apiTokenSecretRef:
            name: cloudflare-api-token
            key: api-token

Performance : Toujours pas terrible

Même sur K3s, le site était encore plus lent. 10-15 secondes pour charger !

Diagnostic

# Analyse des requêtes
curl -w "@curl-format.txt" https://home-fonta.fr
time_namelookup:  0.004
time_connect:  0.012
time_appconnect:  0.043
time_pretransfer:  0.043
time_redirect:  0.000
time_starttransfer:  10.234  # 10 secondes !
time_total:  15.123

Le problème : Flask servait les fichiers statiques !

Solution architecturale : NGINX + Flask

Il fallait séparer :

  • NGINX : Sert les fichiers statiques (CSS, JS, images)
  • Flask : Gère uniquement le dynamique

Nouvelle architecture avec NGINX

# Nouveau Dockerfile avec NGINX + Gunicorn
FROM python:3.12-slim

# Installer NGINX et Supervisor
RUN apt-get update && apt-get install -y nginx supervisor

WORKDIR /app

# Flask app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
COPY templates/ templates/

# Fichiers statiques pour NGINX
COPY static/ /app/static/

# Config NGINX
COPY nginx/default.conf /etc/nginx/sites-enabled/default

# Config Supervisor (pour lancer NGINX + Gunicorn)
COPY supervisor.conf /etc/supervisor/conf.d/app.conf

EXPOSE 80

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

Configuration NGINX :

server {
    listen 80;
    
    # Fichiers statiques - NGINX direct
    location /static/ {
        alias /app/static/;
        expires 7d;
        add_header Cache-Control "public, immutable";
        gzip on;
    }
    
    # Dynamique - Proxy vers Gunicorn
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
    }
}

Résultat : SPECTACULAIRE !

Métrique Avant (Flask seul) Après (NGINX+Flask)
Temps HTML 10-15s 15ms
Temps CSS 504 timeout 50ms
Temps JS 504 timeout 50ms
Images 504 timeout 100ms

100x plus rapide ! 🚀

Automatisation avec Ansible

J'ai créé des playbooks Ansible pour tout automatiser :

# Structure
ansible-k3s/
├── inventory.yml
├── roles/
│   ├── common/         # Config de base des Pi
│   ├── k3s-master/     # Installation master
│   ├── k3s-worker/     # Installation workers
│   └── k3s-apps/       # Déploiement apps
└── playbooks/
    └── site.yml        # Full installation

Une commande pour tout installer :

ansible-playbook -i inventory.yml playbooks/site.yml

Leçons apprises avec K3s

  1. Architecture matters : NGINX pour statique = game changer
  2. CNI debugging est complexe : NodeSelector peut sauver
  3. ARM64 != x86 : Build sur la bonne architecture
  4. Health probes : Toujours vérifier les ports
  5. K3s est léger : Parfait pour les RPi

État après migration K3s

Cluster 4 nodes opérationnel
3 replicas du site (haute dispo)
SSL automatique avec cert-manager
Performance 100x meilleure
⚠️ Problème CNI contourné mais pas résolu

Le site tournait magnifiquement sur K3s, mais je voulais aller plus loin... Mais c'est là que j'ai tout cassé, on vera ça dans les prochaines parties.


Suite : Partie 5 - L'aventure Helm et les derniers défis

Read more