MKU - TPs

TP 3 Déploiement d'une première application avec Kubernetes

Objectifs
- Déployer une application multi-tiers (frontend + backend)- Comprendre la communication inter-services- Effectuer un rolling update- Effectuer un rollback- Observer le comportement d'un HPA

┌─────────────────────────────────────────────────────────┐
│  Navigateur → Service frontend (NodePort)               │
│                     │                                   │
│                     ▼                                   │
│           Pods Frontend (nginx, 2 replicas)            │
│                     │                                   │
│                     ▼ (via ClusterIP)                   │
│           Pods Backend (api, 2 replicas)               │
└─────────────────────────────────────────────────────────┘

Étape 1 : Créer un namespace dédié

# Créer un namespace pour ce TP
kubectl create namespace tp3

# Vérifier
kubectl get namespaces

# Travailler dans ce namespace par défaut (optionnel)
kubectl config set-context --current --namespace=tp3

Étape 2 : Déployer le backend (API)

Créer le fichier backend.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: tp3
  labels:
    app: backend
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: backend
        version: v1
    spec:
      containers:
        - name: api
          image: hashicorp/http-echo:latest
          args:
            - "-text=Hello depuis le backend v1 - Pod: $(POD_NAME)"
            - "-listen=:8080"
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          livenessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: tp3
spec:
  selector:
    app: backend
  ports:
    - port: 8080
      targetPort: 8080
  type: ClusterIP
kubectl apply -f backend.yaml

# Vérifier le déploiement
kubectl get pods -n tp3 -l app=backend
kubectl get service -n tp3 backend

# Tester le service depuis l'intérieur du cluster
kubectl run test-curl --image=curlimages/curl --rm -it --restart=Never \
  -n tp3 -- curl http://backend:8080

Étape 3 : Déployer le frontend

Créer le fichier frontend.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: tp3
data:
  default.conf: |
    server {
        listen 80;

        location / {
            root /usr/share/nginx/html;
            index index.html;
        }

        location /api/ {
            proxy_pass http://backend:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }

  index.html: |
    <!DOCTYPE html>
    <html>
    <head>
        <title>Formation Kubernetes - TP3</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 40px; }
            .box { background: #e8f4f8; padding: 20px; border-radius: 8px; }
            button { background: #0078d4; color: white; padding: 10px 20px;
                     border: none; border-radius: 4px; cursor: pointer; }
        </style>
    </head>
    <body>
        <h1>Formation Kubernetes - TP3</h1>
        <div class="box">
            <p>Frontend opérationnel !</p>
            <button onclick="callApi()">Appeler le backend</button>
            <div id="result"></div>
        </div>
        <script>
            function callApi() {
                fetch('/api/')
                    .then(r => r.text())
                    .then(t => document.getElementById('result').innerHTML =
                        '<p><strong>Réponse :</strong> ' + t + '</p>');
            }
        </script>
    </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: tp3
  labels:
    app: frontend
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: frontend
        version: v1
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf
            - name: nginx-config
              mountPath: /usr/share/nginx/html/index.html
              subPath: index.html
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 10
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: tp3
spec:
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
kubectl apply -f frontend.yaml

# Vérifier
kubectl get pods -n tp3
kubectl get services -n tp3

# Accéder à l'application
minikube service frontend -n tp3

Étape 4 Vérifier la communication inter-services

# Lancer un pod de debug
kubectl run debug --image=nicolaka/netshoot \
  --rm -it --restart=Never -n tp3 -- bash

# Dans le pod de debug :
# Test DNS
nslookup backend.tp3.svc.cluster.local
nslookup frontend.tp3.svc.cluster.local

# Test connectivité
curl http://backend:8080
curl http://frontend:80

exit
# Inspecter les endpoints des services
kubectl get endpoints -n tp3
# Les IPs des endpoints doivent correspondre aux IPs des pods

kubectl get pods -n tp3 -o wide

Étape 5 Effectuer un Rolling Update

Simuler une mise à jour vers la v2 du backend :

# Mettre à jour l'image directement (méthode impérative)
kubectl set image deployment/backend \
  api=hashicorp/http-echo:0.2.3 \
  -n tp3

# Observer le rolling update en temps réel
kubectl rollout status deployment/backend -n tp3 -w

# Observer les pods pendant le rollout
watch kubectl get pods -n tp3 -l app=backend

Méthode déclarative (recommandée) :

Modifier backend.yaml pour changer le message de la v2 :

# Modifier la ligne args dans backend.yaml :
args:
  - "-text=Hello depuis le backend v2 🎉 - Pod: $(POD_NAME)"
  - "-listen=:8080"
kubectl apply -f backend.yaml

# Suivre le rollout
kubectl rollout status deployment/backend -n tp3

# Vérifier l'historique
kubectl rollout history deployment/backend -n tp3

# Tester que la v2 répond
kubectl run test-v2 --image=curlimages/curl --rm -it --restart=Never \
  -n tp3 -- sh -c 'for i in 1 2 3 4 5; do curl -s http://backend:8080; echo; done'

Étape 6 Effectuer un Rollback

# Simuler un problème : déployer une "mauvaise" image
kubectl set image deployment/backend \
  api=hashicorp/http-echo:version-inexistante \
  -n tp3

# Observer l'échec
kubectl rollout status deployment/backend -n tp3
# Attendre quelques instants...

kubectl get pods -n tp3 -l app=backend
# Certains pods seront en ImagePullBackOff

# Rollback immédiat vers la révision précédente
kubectl rollout undo deployment/backend -n tp3

# Vérifier la récupération
kubectl rollout status deployment/backend -n tp3
kubectl get pods -n tp3 -l app=backend

# Voir l'historique complet
kubectl rollout history deployment/backend -n tp3

# Rollback vers une révision spécifique
kubectl rollout undo deployment/backend --to-revision=1 -n tp3

Étape 7 Observer le scaling

# Scaler manuellement
kubectl scale deployment frontend -n tp3 --replicas=4
kubectl get pods -n tp3 -l app=frontend -w

# Revenir à 2 replicas
kubectl scale deployment frontend -n tp3 --replicas=2

# Configurer l'HPA (nécessite metrics-server)
minikube addons enable metrics-server

kubectl autoscale deployment backend \
  --cpu-percent=50 \
  --min=2 \
  --max=6 \
  -n tp3

# Vérifier l'HPA
kubectl get hpa -n tp3

# Générer de la charge (dans un autre terminal)
kubectl run load-generator \
  --image=busybox \
  --restart=Never \
  -n tp3 -- \
  sh -c "while true; do wget -q -O- http://backend:8080; done"

# Observer le scaling automatique
watch kubectl get hpa,pods -n tp3 -l app=backend

# Arrêter le générateur de charge
kubectl delete pod load-generator -n tp3

Étape 8 Examiner les ressources créées

# Vue d'ensemble de tout le namespace
kubectl get all -n tp3

# Détail du deployment backend
kubectl describe deployment backend -n tp3

# Labels et selectors
kubectl get pods -n tp3 --show-labels

# Filtrer par label
kubectl get pods -n tp3 -l app=backend,version=v1

# Voir les logs de tous les pods frontend
kubectl logs -l app=frontend -n tp3 --all-containers --prefix

# Voir les events du namespace
kubectl get events -n tp3 --sort-by='.lastTimestamp'

Étape 9 Nettoyage

# Supprimer toutes les ressources du namespace
kubectl delete namespace tp3

# Ou supprimer fichier par fichier
kubectl delete -f frontend.yaml
kubectl delete -f backend.yaml

✅ Résultat attendu final

À la fin de ce TP, vous avez :

  • Déployé une architecture 2 tiers (frontend Nginx + backend API)
  • Validé la communication inter-services via DNS Kubernetes
  • Effectué un rolling update sans interruption de service
  • Effectué un rollback d'urgence
  • Configuré et observé un HPA

🔧 Dépannage courant

Problème Solution
Frontend ne joint pas le backend kubectl get endpoints -n tp3 - vérifier que backend a des endpoints
HPA bloqué à <unknown> minikube addons enable metrics-server et attendre 2-3 min
Pod en Pending kubectl describe pod <name> -n tp3 → Events
minikube service ne s'ouvre pas Utiliser minikube service frontend -n tp3 --url + curl

TP 7a Déploiement Application & Base de Données (Non Persistante)

Durée estimée : 45 minutes
Environnement : Minikube (local)
Prérequis : TP 3 complété

Objectifs

  • Déployer WordPress + MySQL sur Kubernetes
  • Comprendre la communication via Services ClusterIP
  • Injecter la configuration via des Secrets
  • Observer ce qui se passe en cas de redémarrage (données perdues)

Architecture cible

┌─────────────────────────────────────────────────────────────┐
│  Utilisateur → WordPress Service (NodePort :30080)          │
│                       │                                     │
│               WordPress Pod (1 replica)                     │
│                       │ (ClusterIP mysql:3306)              │
│               MySQL Pod (1 replica)                         │
│               ⚠️ Données dans emptyDir = NON PERSISTANT     │ 
└─────────────────────────────────────────────────────────────┘

Étape 1 Créer le namespace

kubectl create namespace tp7a
kubectl config set-context --current --namespace=tp7a

Étape 2 Créer les Secrets

Les mots de passe ne doivent jamais être en clair dans les YAML de Deployment.

# Créer le secret MySQL
kubectl create secret generic mysql-secret \
  --from-literal=MYSQL_ROOT_PASSWORD=rootpassword123 \
  --from-literal=MYSQL_DATABASE=wordpress \
  --from-literal=MYSQL_USER=wordpress \
  --from-literal=MYSQL_PASSWORD=wppassword123 \
  -n tp7a

# Vérifier (les valeurs sont encodées en base64)
kubectl get secret mysql-secret -n tp7a -o yaml

# Décoder une valeur pour vérifier
kubectl get secret mysql-secret -n tp7a \
  -o jsonpath='{.data.MYSQL_PASSWORD}' | base64 -d
echo  # Newline

Étape 3 Déployer MySQL (sans volume persistant)

Créer le fichier mysql.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: tp7a
  labels:
    app: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
          envFrom:
            - secretRef:
                name: mysql-secret
          resources:
            requests:
              cpu: 200m
              memory: 512Mi
            limits:
              cpu: 500m
              memory: 1Gi
          livenessProbe:
            exec:
              command:
                - mysqladmin
                - ping
                - -h
                - localhost
                - -u
                - root
                - -p$(MYSQL_ROOT_PASSWORD)
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
          readinessProbe:
            exec:
              command:
                - mysql
                - -h
                - localhost
                - -u
                - wordpress
                - -p$(MYSQL_PASSWORD)
                - -e
                - "SELECT 1"
            initialDelaySeconds: 10
            periodSeconds: 5
          # ⚠️ PAS de persistenceVolumeClaim !
          # Les données sont stockées dans le système de fichiers
          # du conteneur = PERDUES au redémarrage du pod
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: tp7a
spec:
  selector:
    app: mysql
  ports:
    - port: 3306
      targetPort: 3306
  type: ClusterIP
  # ClusterIP : accessible uniquement depuis l'intérieur du cluster
kubectl apply -f mysql.yaml

# Attendre que MySQL soit prêt (peut prendre 30-60 secondes)
kubectl wait --for=condition=ready pod \
  -l app=mysql -n tp7a --timeout=120s

kubectl get pods -n tp7a

Étape 4 Déployer WordPress

Créer le fichier wordpress.yaml :

kubectl apply -f wordpress.yaml

# Suivre le démarrage
kubectl get pods -n tp7a -w
# Attendre que les deux pods soient en Running+Ready

# Vérifier les logs WordPress
kubectl logs -l app=wordpress -n tp7a
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: tp7a
  labels:
    app: wordpress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
        - name: wordpress
          image: wordpress:6.4-php8.2-apache
          ports:
            - containerPort: 80
          env:
            - name: WORDPRESS_DB_HOST
              value: mysql:3306          # Résolution DNS du service MySQL
            - name: WORDPRESS_DB_NAME
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_DATABASE
            - name: WORDPRESS_DB_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: WORDPRESS_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /wp-login.php
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 6
---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: tp7a
spec:
  selector:
    app: wordpress
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080
  type: NodePort

Étape 5 Accéder à WordPress

# Obtenir l'URL
minikube service wordpress -n tp7a --url

# Ou directement
MINIKUBE_IP=$(minikube ip)
echo "WordPress: http://${MINIKUBE_IP}:30080"
  1. Ouvrir l'URL dans le navigateur
  2. Configurer WordPress (titre, admin, mot de passe)
  3. Créer un article de test avec du contenu reconnaissable
  4. Publier l'article

Étape 6 Démontrer la perte de données

# Vérifier l'état actuel
kubectl get pods -n tp7a

# Supprimer le pod MySQL (Kubernetes va le recréer automatiquement)
kubectl delete pod -l app=mysql -n tp7a

# Observer la recréation
kubectl get pods -n tp7a -w
# Attendre que le nouveau pod MySQL soit Ready
# Attendre que WordPress soit à nouveau accessible
kubectl wait --for=condition=ready pod \
  -l app=wordpress -n tp7a --timeout=120s

# Recharger WordPress dans le navigateur

Observation : WordPress redirige vers l'installation ! Toutes les données sont perdues car MySQL stockait ses données dans le système de fichiers du conteneur, qui n'existe plus.

⚠️ Conclusion : Sans volume persistant, toute donnée créée dans un conteneur est perdue dès que le pod est recréé ou redémarré. C'est pourquoi les bases de données nécessitent des Persistent Volume Claims (voir TP 8).

Étape 7 Comprendre les services et le DNS

# Inspecter le service MySQL
kubectl describe service mysql -n tp7a
# ClusterIP assignée automatiquement

# Tester la résolution DNS depuis WordPress
kubectl exec -it $(kubectl get pods -l app=mysql -n tp7a \
  -o jsonpath='{.items[0].metadata.name}') -n tp7a -- bash

# Dans le conteneur WordPress :
apt-get update -q && apt-get install -y -q dnsutils
nslookup mysql
# Résout vers la ClusterIP du service

nslookup mysql.tp7a.svc.cluster.local
# Forme complète du nom FQDN

mysql -h mysql -u wordpress -pwppassword123 -e "SHOW DATABASES;"
exit

Étape 8 Inspecter les secrets en détail

# Les secrets ne sont pas affichés par défaut
kubectl get pods -n tp7a -o yaml | grep -A5 "env:"

# Vérifier que les variables d'environnement sont bien injectées
kubectl exec -it $(kubectl get pods -l app=mysql -n tp7a \
  -o jsonpath='{.items[0].metadata.name}') -n tp7a -- \
  env | grep MYSQL

Étape 9 Nettoyage

kubectl delete namespace tp7a

✅ Résultat attendu final

  • WordPress et MySQL déployés et communicants
  • Configuration via Secrets (pas de credentials en clair)
  • Compréhension de la perte de données sans PVC
  • Maîtrise du DNS Kubernetes inter-services

🔧 Dépannage courant

Problème Solution
MySQL en CrashLoopBackOff kubectl logs -l app=mysql -n tp7a souvent un problème de mémoire
WordPress ne joint pas MySQL Vérifier kubectl get endpoints mysql -n tp7a
Page WordPress blanche kubectl logs -l app=wordpress -n tp7a
Probe MySQL échoue Attendre 60s, MySQL démarre lentement

TP 7b Mise en place d'un environnement AKS

Durée estimée : 1h30
Environnement : Azure / AKS
Prérequis : Abonnement Azure, Azure CLI installé

Objectifs

  • Créer un cluster AKS avec Azure CLI
  • Configurer Azure Container Registry (ACR)
  • Builder et pousser une image Docker sur ACR
  • Déployer l'application sur AKS avec un Service LoadBalancer
  • Configurer l'autoscaling des nœuds

Étape 1 Prérequis et connexion Azure

# Installer Azure CLI (si pas déjà fait)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Se connecter
az login

# Vérifier la souscription active
az account show

# Si nécessaire, changer de souscription
az account list --output table
az account set --subscription "<subscription-id>"

# Enregistrer les providers nécessaires
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.ContainerRegistry

Étape 2 Créer le Resource Group et les variables

# Variables d'environnement (adapter selon votre contexte)
export RESOURCE_GROUP="rg-formation-kubernetes"
export LOCATION="westeurope"
export CLUSTER_NAME="aks-formation"
export ACR_NAME="acrformation$(date +%s | tail -c5)"   # Nom unique
export NODE_COUNT=2
export NODE_VM_SIZE="Standard_D2s_v3"

# Créer le Resource Group
az group create \
  --name $RESOURCE_GROUP \
  --location $LOCATION

echo "Resource Group créé : $RESOURCE_GROUP"
echo "ACR Name : $ACR_NAME"

Étape 3 Créer Azure Container Registry (ACR)

# Créer l'ACR
az acr create \
  --resource-group $RESOURCE_GROUP \
  --name $ACR_NAME \
  --sku Basic \
  --location $LOCATION

# Récupérer le login server
ACR_LOGIN_SERVER=$(az acr show \
  --name $ACR_NAME \
  --resource-group $RESOURCE_GROUP \
  --query loginServer \
  --output tsv)

echo "ACR Login Server : $ACR_LOGIN_SERVER"

# Se connecter à l'ACR
az acr login --name $ACR_NAME

Étape 4 Builder et pousser une image custom

Créer l'application à conteneuriser :

mkdir -p /tmp/my-api && cd /tmp/my-api

Créer app.py :

# app.py
from flask import Flask, jsonify
import os
import socket

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        "message": "Hello depuis AKS !",
        "pod_name": socket.gethostname(),
        "version": os.getenv("APP_VERSION", "1.0"),
        "environment": os.getenv("APP_ENV", "production")
    })

@app.route('/health')
def health():
    return jsonify({"status": "healthy"})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Créer requirements.txt :

flask==3.0.0

Créer Dockerfile :

FROM python:3.11-slim

WORKDIR /app

# Installer les dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier l'application
COPY app.py .

# Utilisateur non-root
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 5000

CMD ["python", "app.py"]
# Builder et pousser l'image
docker build -t my-api:1.0 .

# Tagger pour l'ACR
docker tag my-api:1.0 ${ACR_LOGIN_SERVER}/my-api:1.0

# Pousser sur ACR
docker push ${ACR_LOGIN_SERVER}/my-api:1.0

# Vérifier que l'image est dans l'ACR
az acr repository list --name $ACR_NAME --output table
az acr repository show-tags \
  --name $ACR_NAME \
  --repository my-api \
  --output table

Étape 5 Créer le cluster AKS

# Créer le cluster AKS
az aks create \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --node-count $NODE_COUNT \
  --node-vm-size $NODE_VM_SIZE \
  --attach-acr $ACR_NAME \
  --enable-cluster-autoscaler \
  --min-count 1 \
  --max-count 5 \
  --enable-managed-identity \
  --network-plugin azure \
  --network-policy azure \
  --generate-ssh-keys \
  --location $LOCATION

# ⏳ Cette commande prend environ 5-10 minutes

# Vérifier le statut
az aks show \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --query provisioningState \
  --output tsv

Étape 6 Se connecter au cluster

# Récupérer les credentials
az aks get-credentials \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME

# Vérifier la connexion
kubectl cluster-info
kubectl get nodes -o wide

# Voir les informations du cluster
az aks show \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --output table

Étape 7 Déployer l'application sur AKS

Créer my-api-aks.yaml :

apiVersion: v1
kind: Namespace
metadata:
  name: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
  namespace: production
  labels:
    app: my-api
    version: "1.0"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-api
        version: "1.0"
    spec:
      containers:
        - name: my-api
          # Remplacer ACR_LOGIN_SERVER par la valeur réelle
          image: ACR_LOGIN_SERVER/my-api:1.0
          ports:
            - containerPort: 5000
          env:
            - name: APP_VERSION
              value: "1.0"
            - name: APP_ENV
              value: "production"
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /health
              port: 5000
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 5000
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-api
  namespace: production
spec:
  selector:
    app: my-api
  ports:
    - port: 80
      targetPort: 5000
  type: LoadBalancer
  # AKS va automatiquement créer un Azure Load Balancer
  # et assigner une IP publique
# Remplacer le placeholder par la vraie valeur ACR
sed -i "s|ACR_LOGIN_SERVER|${ACR_LOGIN_SERVER}|g" my-api-aks.yaml

# Déployer
kubectl apply -f my-api-aks.yaml

# Suivre le déploiement
kubectl get pods -n production -w

Étape 8 Accéder via le LoadBalancer Azure

# Attendre l'attribution de l'IP publique (peut prendre 2-3 minutes)
kubectl get service my-api -n production -w

# Quand l'EXTERNAL-IP n'est plus <pending>, tester :
EXTERNAL_IP=$(kubectl get service my-api -n production \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "URL de l'application : http://${EXTERNAL_IP}"

# Tester
curl http://${EXTERNAL_IP}

# Résultat attendu :
# {
#   "environment": "production",
#   "message": "Hello depuis AKS !",
#   "pod_name": "my-api-abc123-xyz",
#   "version": "1.0"
# }

# Appeler plusieurs fois pour voir la répartition de charge
for i in {1..10}; do
  curl -s http://${EXTERNAL_IP} | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['pod_name'])"
done

Étape 9 Effectuer une mise à jour sur AKS

cd /tmp/my-api

# Modifier l'application (v2)
cat > app.py << 'EOF'
from flask import Flask, jsonify
import os
import socket

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        "message": "Hello depuis AKS - Version 2.0 🚀",
        "pod_name": socket.gethostname(),
        "version": os.getenv("APP_VERSION", "2.0"),
        "environment": os.getenv("APP_ENV", "production"),
        "features": ["feature-a", "feature-b"]  # Nouveau !
    })

@app.route('/health')
def health():
    return jsonify({"status": "healthy", "version": "2.0"})
EOF

# Builder et pousser la v2
docker build -t my-api:2.0 .
docker tag my-api:2.0 ${ACR_LOGIN_SERVER}/my-api:2.0
docker push ${ACR_LOGIN_SERVER}/my-api:2.0

# Mettre à jour le déploiement AKS
kubectl set image deployment/my-api \
  my-api=${ACR_LOGIN_SERVER}/my-api:2.0 \
  -n production

# Observer le rolling update
kubectl rollout status deployment/my-api -n production

# Vérifier la v2
curl http://${EXTERNAL_IP}

Étape 10 Observer l'autoscaling AKS

# Vérifier la configuration de l'autoscaler
kubectl get nodes
az aks show \
  --resource-group $RESOURCE_GROUP \
  --name $CLUSTER_NAME \
  --query "agentPoolProfiles[0].{minCount:minCount,maxCount:maxCount,count:count}" \
  --output table

# Configurer l'HPA
kubectl autoscale deployment my-api \
  --cpu-percent=50 \
  --min=2 \
  --max=10 \
  -n production

# Vérifier l'HPA
kubectl get hpa -n production

# Voir les métriques (nécessite quelques minutes)
kubectl top pods -n production
kubectl top nodes

Étape 11 Nettoyage (IMPORTANT — coût Azure)

# Supprimer uniquement les ressources K8s
kubectl delete namespace production

# Ou supprimer tout le Resource Group (supprimer le cluster et l'ACR)
az group delete \
  --name $RESOURCE_GROUP \
  --yes \
  --no-wait

echo "Suppression en cours..."

✅ Résultat attendu final

  • Cluster AKS opérationnel avec autoscaler
  • ACR configuré et lié au cluster
  • Application déployée accessible via IP publique Azure
  • Rolling update effectué sans downtime
  • Maîtrise des commandes az + kubectl combinées

🔧 Dépannage courant

Problème Solution
ImagePullBackOff sur AKS Vérifier --attach-acr lors de la création du cluster
LoadBalancer IP reste <pending> Attendre 3-5 min, vérifier quotas Azure
az aks create échoue Vérifier les providers enregistrés
Cluster ne répond plus az aks get-credentials --overwrite-existing

TP 8 Déploiement Application & Base de Données sur AKS (Persistante)

Durée estimée : 1 heure
Environnement : AKS
Prérequis : TP 7b complété, cluster AKS opérationnel

Objectifs

  • Comprendre les Persistent Volumes et PVC sur AKS
  • Utiliser les StorageClass Azure (Azure Disk, Azure Files)
  • Déployer WordPress + MySQL avec des données persistantes
  • Valider la persistance après redémarrage des pods

Architecture cible

┌──────────────────────────────────────────────────────────────┐
│  Internet → Azure Load Balancer → WordPress Service          │
│                                        │                     │
│                              WordPress Pod                   │
│                              Volume: Azure Files (RWX)      │
│                              (fichiers WordPress partagés)  │
│                                        │                     │
│                              MySQL Service (ClusterIP)       │
│                                        │                     │
│                              MySQL Pod                       │
│                              Volume: Azure Disk (RWO)       │
│                              (données BDD persistantes)     │
└──────────────────────────────────────────────────────────────┘

Étape 1 Préparer l'environnement

# Vérifier la connexion au cluster AKS
kubectl cluster-info
kubectl get nodes

# Créer le namespace
kubectl create namespace tp8
kubectl config set-context --current --namespace=tp8

# Voir les StorageClasses disponibles sur AKS
kubectl get storageclass

StorageClasses AKS par défaut :

StorageClass Type Access Mode Usage
default Azure Disk (HDD) RWO BDD, données critiques
managed-premium Azure Disk (SSD) RWO BDD performance
azurefile Azure Files (SMB) RWX Fichiers partagés
azurefile-premium Azure Files SSD RWX Fichiers haute perf
# Inspecter la StorageClass par défaut
kubectl describe storageclass default
kubectl describe storageclass azurefile

Étape 2 Créer les Secrets

kubectl create secret generic mysql-secret \
  --from-literal=MYSQL_ROOT_PASSWORD=rootSecure123! \
  --from-literal=MYSQL_DATABASE=wordpress \
  --from-literal=MYSQL_USER=wordpress \
  --from-literal=MYSQL_PASSWORD=wpSecure123! \
  -n tp8

kubectl get secret mysql-secret -n tp8

Étape 3 Créer les Persistent Volume Claims

Créer le fichier storage.yaml :

# PVC pour MySQL : Azure Disk (ReadWriteOnce)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  namespace: tp8
  labels:
    app: mysql
spec:
  accessModes:
    - ReadWriteOnce          # Un seul nœud à la fois (suffisant pour MySQL)
  storageClassName: managed-premium
  resources:
    requests:
      storage: 10Gi
---
# PVC pour WordPress : Azure Files (ReadWriteMany)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-pvc
  namespace: tp8
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteMany          # Plusieurs pods peuvent monter ce volume
  storageClassName: azurefile
  resources:
    requests:
      storage: 5Gi
kubectl apply -f storage.yaml

# Vérifier les PVC
kubectl get pvc -n tp8
# STATUS: Pending au début, puis Bound une fois le volume créé

# Attendre que les PVC soient Bound
kubectl wait --for=jsonpath='{.status.phase}'=Bound \
  pvc/mysql-pvc -n tp8 --timeout=120s

kubectl wait --for=jsonpath='{.status.phase}'=Bound \
  pvc/wordpress-pvc -n tp8 --timeout=120s

kubectl get pvc -n tp8
# NAME            STATUS   VOLUME                                     CAPACITY
# mysql-pvc       Bound    pvc-abc123-...                             10Gi
# wordpress-pvc   Bound    pvc-def456-...                             5Gi

# Voir le PV créé automatiquement
kubectl get pv | grep tp8

Étape 4 Déployer MySQL avec PVC

Créer le fichier mysql-persistent.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: tp8
  labels:
    app: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate       # Important pour MySQL avec RWO !
    # RollingUpdate échouerait car le PVC RWO ne peut
    # être monté que par un pod à la fois
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
          envFrom:
            - secretRef:
                name: mysql-secret
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi
          livenessProbe:
            exec:
              command:
                - mysqladmin
                - ping
                - -h
                - localhost
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - mysql
                - -h
                - localhost
                - -u
                - root
                - -p$(MYSQL_ROOT_PASSWORD)
                - -e
                - "SELECT 1"
            initialDelaySeconds: 15
            periodSeconds: 5
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql    # Répertoire données MySQL
      volumes:
        - name: mysql-data
          persistentVolumeClaim:
            claimName: mysql-pvc           # Référencer le PVC créé
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: tp8
spec:
  selector:
    app: mysql
  ports:
    - port: 3306
      targetPort: 3306
  type: ClusterIP
kubectl apply -f mysql-persistent.yaml

# Attendre que MySQL soit prêt
kubectl rollout status deployment/mysql -n tp8 --timeout=120s
kubectl get pods -n tp8 -l app=mysql

# Vérifier que le volume est monté
kubectl describe pod -l app=mysql -n tp8 | grep -A 10 "Volumes:"

Étape 5 Déployer WordPress avec PVC

Créer le fichier wordpress-persistent.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  namespace: tp8
  labels:
    app: wordpress
spec:
  replicas: 2              # 2 replicas possibles grâce à Azure Files (RWX)
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
        - name: wordpress
          image: wordpress:6.4-php8.2-apache
          ports:
            - containerPort: 80
          env:
            - name: WORDPRESS_DB_HOST
              value: mysql:3306
            - name: WORDPRESS_DB_NAME
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_DATABASE
            - name: WORDPRESS_DB_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_USER
            - name: WORDPRESS_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: MYSQL_PASSWORD
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
          readinessProbe:
            httpGet:
              path: /wp-login.php
              port: 80
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 6
          volumeMounts:
            - name: wordpress-data
              mountPath: /var/www/html     # Fichiers WordPress (themes, plugins, uploads)
      volumes:
        - name: wordpress-data
          persistentVolumeClaim:
            claimName: wordpress-pvc       # Azure Files : partagé entre les 2 replicas
---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: tp8
spec:
  selector:
    app: wordpress
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
kubectl apply -f wordpress-persistent.yaml

# Suivre le déploiement
kubectl get pods -n tp8 -w

# Vérifier que les 2 pods WordPress partagent bien le même volume
kubectl describe pods -l app=wordpress -n tp8 | grep -A5 "Volumes:"

Étape 6 Accéder et configurer WordPress

# Attendre l'IP LoadBalancer
kubectl get service wordpress -n tp8 -w

EXTERNAL_IP=$(kubectl get service wordpress -n tp8 \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "WordPress : http://${EXTERNAL_IP}"
  1. Ouvrir dans le navigateur
  2. Configurer WordPress (langue, titre, admin)
  3. Créer un article de test avec du contenu
  4. Uploader une image dans la médiathèque (teste le volume partagé)

Étape 7 Valider la persistance

# Supprimer le pod MySQL (simulation d'un crash)
kubectl delete pod -l app=mysql -n tp8

# Observer la recréation automatique
kubectl get pods -n tp8 -w
# Attendre que MySQL soit de nouveau Ready

# Recharger WordPress → Les données doivent être présentes !
curl -s http://${EXTERNAL_IP} | grep -o '<title>.*</title>'
# Supprimer TOUS les pods
kubectl delete pods --all -n tp8

# Observer la recréation
kubectl get pods -n tp8 -w
# Attendre que tout soit Ready

# Vérifier que WordPress et les données sont toujours là
curl http://${EXTERNAL_IP}
Conclusion : Contrairement au TP 7a, les données sont maintenant persistantes grâce aux PVC Azure !

Étape 8 Explorer les volumes Azure dans le portail

# Voir les détails des PV et PVC
kubectl get pv,pvc -n tp8 -o wide

# Voir le nom du disque Azure pour MySQL
kubectl get pv $(kubectl get pvc mysql-pvc -n tp8 \
  -o jsonpath='{.spec.volumeName}') \
  -o jsonpath='{.spec.csi.volumeHandle}'

# Voir le share Azure Files pour WordPress
kubectl get pv $(kubectl get pvc wordpress-pvc -n tp8 \
  -o jsonpath='{.spec.volumeName}') \
  -o jsonpath='{.spec.csi.volumeAttributes}'

Vous pouvez aussi vérifier dans le portail Azure :

  • Azure Disks dans le Resource Group des nœuds AKS (MC_*)
  • Azure Storage Accounts pour les Azure Files

Étape 9 Expansion d'un volume (Azure Disk)

# Vérifier que la StorageClass supporte l'expansion
kubectl get storageclass managed-premium \
  -o jsonpath='{.allowVolumeExpansion}'
# true

# Agrandir le PVC MySQL de 10Gi à 20Gi
kubectl patch pvc mysql-pvc -n tp8 \
  --type='merge' \
  -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

# Suivre l'expansion
kubectl get pvc mysql-pvc -n tp8 -w
# CAPACITY passe de 10Gi à 20Gi (peut prendre quelques minutes)

Étape 10 Nettoyage

# Supprimer les ressources Kubernetes
kubectl delete namespace tp8

# Les PV avec ReclaimPolicy: Delete sont supprimés automatiquement
# (les disques Azure et shares Files sont supprimés)
kubectl get pv | grep Released  # Vérifier qu'il ne reste pas de PV orphelins

# Supprimer les PV orphelins si nécessaire
kubectl delete pv <pv-name>

✅ Résultat attendu final

  • PVC Azure Disk pour MySQL (10Gi, RWO, SSD)
  • PVC Azure Files pour WordPress (5Gi, RWX, partagé entre 2 pods)
  • Données persistantes après suppression/recréation des pods
  • Maîtrise des StorageClasses AKS

🔧 Dépannage courant

Problème Solution
PVC reste en Pending Vérifier StorageClass : kubectl describe pvc
MySQL crash avec strategy: Recreate Normal pendant le rollout, attendre
WordPress ne démarre pas Le PVC Azure Files peut prendre 2-3 min à monter
Multi-Attach error sur le PVC MySQL Utiliser strategy: Recreate, pas RollingUpdate
Disque Azure non supprimé après delete kubectl get pv → supprimer manuellement si Released

TP 9 Mise en place d'une politique Ingress sur AKS

Durée estimée : 1 heure
Environnement : AKS
Prérequis : TP 7b complété, cluster AKS opérationnel

Objectifs

  • Installer le contrôleur Ingress NGINX sur AKS
  • Configurer le routage par chemin (path-based routing)
  • Configurer le routage par domaine (host-based routing)
  • Activer TLS avec cert-manager
  • Configurer des annotations avancées (rate limiting, redirections)

Architecture cible

Internet
    │
    ▼
Azure Load Balancer (1 seule IP publique)
    │
    ▼
NGINX Ingress Controller
    │
    ├── /api/*         →  backend-service:8080
    ├── /              →  frontend-service:80
    │
    └── host: app.example.com  →  autre-service:80

Étape 1 Préparer l'environnement

# Vérifier la connexion AKS
kubectl get nodes

# Créer les namespaces
kubectl create namespace production
kubectl create namespace ingress-nginx

Étape 2 Installer NGINX Ingress Controller

# Ajouter le repo Helm
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Installer NGINX Ingress Controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --set controller.replicaCount=2 \
  --set controller.nodeSelector."kubernetes\.io/os"=linux \
  --set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \
  --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz

# Attendre le démarrage
kubectl get pods -n ingress-nginx -w
kubectl rollout status deployment/nginx-ingress-ingress-nginx-controller \
  -n ingress-nginx --timeout=120s

# Récupérer l'IP publique du contrôleur
kubectl get service -n ingress-nginx nginx-ingress-ingress-nginx-controller
# Attendre que EXTERNAL-IP soit assignée (2-3 minutes)

INGRESS_IP=$(kubectl get service \
  nginx-ingress-ingress-nginx-controller \
  -n ingress-nginx \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "Ingress IP : ${INGRESS_IP}"

Étape 3 Déployer les applications de test

Créer le fichier apps.yaml :

# Application Frontend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: frontend
          image: hashicorp/http-echo:latest
          args:
            - "-text=<h1>Frontend</h1><p>Bienvenue sur la page d'accueil !</p>"
            - "-listen=:5678"
          ports:
            - containerPort: 5678
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: production
spec:
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 5678
  type: ClusterIP
---
# Application Backend/API
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - name: backend
          image: hashicorp/http-echo:latest
          args:
            - '-text={"status": "ok", "service": "backend-api", "pod": "'"$(hostname)"'"}'
            - "-listen=:5678"
          ports:
            - containerPort: 5678
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: production
spec:
  selector:
    app: backend
  ports:
    - port: 8080
      targetPort: 5678
  type: ClusterIP
---
# Application secondaire (pour le routage par host)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-secondaire
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-secondaire
  template:
    metadata:
      labels:
        app: app-secondaire
    spec:
      containers:
        - name: app
          image: hashicorp/http-echo:latest
          args:
            - "-text=<h1>Application Secondaire</h1><p>Accessible via un sous-domaine différent</p>"
            - "-listen=:5678"
          ports:
            - containerPort: 5678
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
---
apiVersion: v1
kind: Service
metadata:
  name: app-secondaire
  namespace: production
spec:
  selector:
    app: app-secondaire
  ports:
    - port: 80
      targetPort: 5678
  type: ClusterIP
kubectl apply -f apps.yaml
kubectl get pods -n production
kubectl get services -n production

Étape 4 Routage par chemin (Path-based Routing)

Créer le fichier ingress-path.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: path-routing
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2    # Réécriture du chemin
spec:
  ingressClassName: nginx
  rules:
    - http:
        paths:
          # Trafic /api/* → backend (en retirant le préfixe /api)
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: backend
                port:
                  number: 8080

          # Trafic / → frontend
          - path: /()(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: frontend
                port:
                  number: 80
kubectl apply -f ingress-path.yaml

# Vérifier l'Ingress
kubectl get ingress -n production
kubectl describe ingress path-routing -n production

# Tester le routage (utiliser l'INGRESS_IP récupérée plus tôt)
curl http://${INGRESS_IP}/
curl http://${INGRESS_IP}/api/
curl http://${INGRESS_IP}/api/users

Étape 5 Routage par domaine (Host-based Routing)

Pour ce TP, utiliser nip.io pour simuler des domaines sans DNS réel :

# nip.io permet de créer des domaines du type : <ip>.nip.io
# Exemple : 20.31.45.67.nip.io → résout vers 20.31.45.67

echo "Domaine principal : ${INGRESS_IP}.nip.io"
echo "Sous-domaine : secondaire.${INGRESS_IP}.nip.io"

Créer le fichier ingress-host.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: host-routing
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
    # Domaine principal
    - host: "${INGRESS_IP}.nip.io"     # Remplacer avec la vraie IP
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: backend
                port:
                  number: 8080
          - path: /()(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: frontend
                port:
                  number: 80

    # Sous-domaine pour l'app secondaire
    - host: "secondaire.${INGRESS_IP}.nip.io"
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-secondaire
                port:
                  number: 80
# Remplacer les placeholders avec l'IP réelle
sed -i "s/\${INGRESS_IP}/${INGRESS_IP}/g" ingress-host.yaml

kubectl apply -f ingress-host.yaml

# Tester le routage par host
curl http://${INGRESS_IP}.nip.io/
curl http://${INGRESS_IP}.nip.io/api/
curl http://secondaire.${INGRESS_IP}.nip.io/

Étape 6 TLS avec cert-manager

# Installer cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

# Attendre le démarrage
kubectl rollout status deployment/cert-manager \
  -n cert-manager --timeout=120s

kubectl get pods -n cert-manager

Créer le fichier cluster-issuer.yaml :

# ClusterIssuer Let's Encrypt (staging = pour les tests, pas de vraie limite)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: formation@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
      - http01:
          ingress:
            class: nginx
kubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer

Créer le fichier ingress-tls.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - "INGRESS_IP.nip.io"
      secretName: app-tls-staging   # cert-manager crée ce Secret automatiquement
  rules:
    - host: "INGRESS_IP.nip.io"
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: backend
                port:
                  number: 8080
          - path: /()(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: frontend
                port:
                  number: 80
sed -i "s/INGRESS_IP/${INGRESS_IP}/g" ingress-tls.yaml
kubectl apply -f ingress-tls.yaml

# Suivre l'émission du certificat
kubectl get certificate -n production -w
kubectl describe certificate app-tls-staging -n production

# Vérifier le challenge ACME
kubectl get challenges -n production

# Test HTTPS (le certificat staging n'est pas approuvé par les navigateurs)
curl -k https://${INGRESS_IP}.nip.io/
curl -k https://${INGRESS_IP}.nip.io/api/

Étape 7 Annotations avancées

Créer le fichier ingress-advanced.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: advanced-ingress
  namespace: production
  annotations:
    # Rate limiting : 10 requêtes par minute par IP
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-connections: "5"

    # Timeouts
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "30"

    # Headers CORS
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://myapp.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"

    # Security headers
    nginx.ingress.kubernetes.io/configuration-snippet: |
      add_header X-Frame-Options "SAMEORIGIN" always;
      add_header X-Content-Type-Options "nosniff" always;
      add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Redirection HTTP → HTTPS
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"

    # Body size max (pour les uploads)
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"

    # Sticky sessions (affinité de session)
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "route"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"

spec:
  ingressClassName: nginx
  rules:
    - host: "INGRESS_IP.nip.io"
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 80
sed -i "s/INGRESS_IP/${INGRESS_IP}/g" ingress-advanced.yaml
kubectl apply -f ingress-advanced.yaml

# Tester le rate limiting
for i in {1..15}; do
  echo -n "Requête $i: "
  curl -s -o /dev/null -w "%{http_code}" http://${INGRESS_IP}.nip.io/
  echo
done
# Les requêtes 11+ doivent retourner 429 (Too Many Requests)

# Tester les headers de sécurité
curl -I http://${INGRESS_IP}/
# Vérifier X-Frame-Options, X-Content-Type-Options, etc.

Étape 8 Inspecter la configuration NGINX générée

# Voir la configuration NGINX générée par le contrôleur
kubectl exec -n ingress-nginx \
  $(kubectl get pods -n ingress-nginx \
    -l app.kubernetes.io/name=ingress-nginx \
    -o jsonpath='{.items[0].metadata.name}') \
  -- nginx -T 2>/dev/null | grep -A 20 "location /api"

# Voir les logs du contrôleur Ingress
kubectl logs -n ingress-nginx \
  -l app.kubernetes.io/name=ingress-nginx \
  --tail=50

# Voir les métriques NGINX
kubectl exec -n ingress-nginx \
  $(kubectl get pods -n ingress-nginx \
    -l app.kubernetes.io/name=ingress-nginx \
    -o jsonpath='{.items[0].metadata.name}') \
  -- curl -s localhost:10254/metrics | grep nginx_ingress_requests

Étape 9 Nettoyage

kubectl delete namespace production
helm uninstall nginx-ingress -n ingress-nginx
helm uninstall cert-manager -n cert-manager
kubectl delete namespace ingress-nginx cert-manager
kubectl delete clusterissuer letsencrypt-staging

✅ Résultat attendu final

  • NGINX Ingress Controller opérationnel sur AKS
  • Routage par chemin : / → frontend, /api/* → backend
  • Routage par host : domaine principal et sous-domaine séparés
  • Certificat TLS émis par cert-manager
  • Rate limiting et headers de sécurité configurés

🔧 Dépannage courant

Problème Solution
Ingress ne route pas kubectl describe ingress -n production + kubectl logs NGINX
Certificate reste en False kubectl describe challenge -n production
502 Bad Gateway Vérifier les services cibles : kubectl get endpoints -n production
Rate limiting trop strict Ajuster limit-rps ou tester avec une autre IP
nip.io ne résout pas Tester avec curl --resolve ou utiliser l'IP directement

TP 11 Logging, Surveillance & Dépannage sur AKS

Durée estimée : 1h30
Environnement : AKS
Prérequis : TP 7b complété, cluster AKS opérationnel

Objectifs

  • Déployer la stack Prometheus + Grafana + Loki sur AKS
  • Configurer Alloy pour la collecte de logs
  • Écrire des requêtes LogQL pour analyser les logs
  • Créer une alerte sur les erreurs applicatives
  • Diagnostiquer des problèmes courants avec kubectl

Partie A Déploiement de la stack d'observabilité

A.1 Créer le namespace monitoring

kubectl create namespace monitoring
kubectl config set-context --current --namespace=monitoring

A.2 Installer Prometheus + Grafana (kube-prometheus-stack)

# Ajouter les repos Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Installer kube-prometheus-stack
helm install prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set grafana.adminPassword=Formation2024! \
  --set grafana.service.type=LoadBalancer \
  --set prometheus.prometheusSpec.retention=7d \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=managed-premium \
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=20Gi

# Attendre le démarrage (peut prendre 3-5 minutes)
kubectl rollout status deployment/prometheus-stack-grafana \
  -n monitoring --timeout=180s

kubectl get pods -n monitoring
# Récupérer l'IP Grafana
GRAFANA_IP=$(kubectl get service prometheus-stack-grafana \
  -n monitoring \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "Grafana : http://${GRAFANA_IP}"
echo "Login : admin / Formation2024!"

A.3 Installer Loki

Créer le fichier loki-values.yaml :

loki:
  commonConfig:
    replication_factor: 1     # Simplifié pour le TP (1 seule réplique)
  storage:
    type: filesystem          # Pour le TP (en prod : Azure Blob Storage)
  schemaConfig:
    configs:
      - from: "2024-01-01"
        store: tsdb
        object_store: filesystem
        schema: v13
        index:
          prefix: loki_index_
          period: 24h

# Désactiver les microservices (mode monolithique pour le TP)
singleBinary:
  replicas: 1

# Désactiver les composants distribués
read:
  replicas: 0
write:
  replicas: 0
backend:
  replicas: 0
helm install loki grafana/loki \
  --namespace monitoring \
  --values loki-values.yaml

kubectl get pods -n monitoring -l app.kubernetes.io/name=loki -w

A.4 Installer Alloy (collecteur de logs)

Créer le fichier alloy-values.yaml :

alloy:
  configMap:
    create: true
    content: |
      // Découverte des pods Kubernetes
      discovery.kubernetes "pods" {
        role = "pod"
      }

      // Relabeling : enrichissement des logs avec les métadonnées K8s
      discovery.relabel "pod_logs" {
        targets = discovery.kubernetes.pods.targets

        // Garder uniquement les pods avec des logs
        rule {
          source_labels = ["__meta_kubernetes_pod_phase"]
          regex         = "Pending|Succeeded|Failed|Completed"
          action        = "drop"
        }

        // Extraire les métadonnées utiles
        rule {
          source_labels = ["__meta_kubernetes_namespace"]
          target_label  = "namespace"
        }

        rule {
          source_labels = ["__meta_kubernetes_pod_name"]
          target_label  = "pod"
        }

        rule {
          source_labels = ["__meta_kubernetes_pod_container_name"]
          target_label  = "container"
        }

        rule {
          source_labels = ["__meta_kubernetes_pod_label_app"]
          target_label  = "app"
        }

        rule {
          source_labels = ["__meta_kubernetes_node_name"]
          target_label  = "node"
        }

        // Chemin vers le fichier de logs du conteneur
        rule {
          source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
          separator     = "/"
          target_label  = "__path__"
          replacement   = "/var/log/pods/*$1/*.log"
        }
      }

      // Collecte des logs depuis les fichiers
      loki.source.kubernetes "pods" {
        targets    = discovery.relabel.pod_logs.output
        forward_to = [loki.write.default.receiver]
      }

      // Envoi vers Loki
      loki.write "default" {
        endpoint {
          url = "http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push"
        }
      }

  # Permissions pour lire les logs des pods
  rbac:
    create: true

  serviceAccount:
    create: true

controller:
  type: daemonset    # Un agent par nœud
  tolerations:
    - key: node-role.kubernetes.io/control-plane
      effect: NoSchedule
helm install alloy grafana/alloy \
  --namespace monitoring \
  --values alloy-values.yaml

kubectl get daemonset -n monitoring alloy
kubectl get pods -n monitoring -l app.kubernetes.io/name=alloy

A.5 Configurer la datasource Loki dans Grafana

# Accéder à Grafana
echo "Grafana : http://${GRAFANA_IP}"

Dans Grafana :

  1. Connections → Data sources → Add data source
  2. Choisir Loki
  3. URL : http://loki-gateway.monitoring.svc.cluster.local
  4. Cliquer Save & Test → doit retourner "Data source connected"

Partie B Déployer une application de test

B.1 Application générant des logs variés

kubectl create namespace production

Créer le fichier log-generator.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: log-generator
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: log-generator
  template:
    metadata:
      labels:
        app: log-generator
    spec:
      containers:
        - name: logger
          image: python:3.11-slim
          command:
            - python3
            - -c
            - |
              import time, random, json, datetime

              levels = ["INFO", "INFO", "INFO", "WARNING", "ERROR", "INFO", "DEBUG"]
              messages = {
                  "INFO": ["Request processed", "User logged in", "Cache hit", "DB query OK"],
                  "WARNING": ["Slow query: 2.3s", "Memory usage: 75%", "Cache miss"],
                  "ERROR": ["DB connection failed", "Timeout after 30s", "Invalid token"],
                  "DEBUG": ["Processing item 42", "Cache key: user:123"]
              }

              while True:
                  level = random.choice(levels)
                  msg = random.choice(messages[level])
                  log = {
                      "timestamp": datetime.datetime.utcnow().isoformat(),
                      "level": level,
                      "message": msg,
                      "user_id": random.randint(1, 1000),
                      "duration_ms": random.randint(1, 5000)
                  }
                  print(json.dumps(log))
                  time.sleep(random.uniform(0.5, 2))
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
kubectl apply -f log-generator.yaml
kubectl get pods -n production

# Voir les logs générés
kubectl logs -l app=log-generator -n production --follow &
sleep 10
kill %1

Partie C Requêtes LogQL dans Grafana

C.1 Accéder à Grafana Explore

  1. Grafana → Explore (menu gauche, icône boussole)
  2. Sélectionner Loki comme datasource

C.2 Requêtes de base

# Tous les logs du namespace production
{namespace="production"}

# Logs d'une app spécifique
{namespace="production", app="log-generator"}

# Filtrer les erreurs
{namespace="production"} |= "ERROR"

# Exclure les logs DEBUG
{namespace="production"} != "DEBUG"

# Regex : erreurs ou warnings
{namespace="production"} |~ "ERROR|WARNING"

C.3 Parsing des logs JSON

# Parser JSON et filtrer par champ
{namespace="production"} | json | level="ERROR"

# Filtrer par durée (requêtes lentes)
{namespace="production"} | json | duration_ms > 3000

# Afficher uniquement certains champs
{namespace="production"} | json | line_format "{{.level}}: {{.message}} ({{.duration_ms}}ms)"

C.4 Métriques depuis les logs

# Taux d'erreurs sur 5 minutes
rate({namespace="production"} | json | level="ERROR" [5m])

# Comptage par niveau sur 1 heure
sum by (level) (
  count_over_time(
    {namespace="production"} | json [1h]
  )
)

# Durée moyenne des requêtes par pod
avg by (pod) (
  avg_over_time(
    {namespace="production"} | json | unwrap duration_ms [5m]
  )
)

C.5 Exercice pratique

Écrire les requêtes LogQL pour répondre aux questions suivantes :

1. Combien d'erreurs par minute en moyenne ?
   → rate({namespace="production"} | json | level="ERROR" [5m])

2. Quel pod génère le plus d'erreurs ?
   → sum by (pod) (count_over_time({namespace="production"} | json | level="ERROR" [1h]))

3. Quelle est la durée moyenne des requêtes ?
   → avg(avg_over_time({namespace="production"} | json | unwrap duration_ms [10m]))

4. Quels messages d'erreur sont les plus fréquents ?
   → topk(5, sum by (message) (count_over_time({namespace="production"} | json | level="ERROR" [1h])))

Partie D Créer une alerte Grafana sur les logs

D.1 Règle d'alerte dans Grafana

  1. Grafana → Alerting → Alert rules → New alert rule
  2. Configurer :
Name: High Error Rate - Production
Type: Grafana managed alert

Query A (Loki):
{namespace="production"} | json | level="ERROR"

Expression B:
- Type: Reduce
- Function: Last
- Input: A

Expression C (Condition):
- Type: Threshold
- Input: B
- IS ABOVE: 5   (plus de 5 erreurs par intervalle)

Evaluation:
- Evaluate every: 1m
- For: 2m        (alerte si condition vraie pendant 2 min)

D.2 Canal de notification (optionnel)

Alerting → Contact points → Add contact point
Type: Email / Slack / Teams

Partie E Dépannage avec kubectl

E.1 Créer des situations de problème

# Créer un pod avec une image inexistante
kubectl run bad-pod \
  --image=image-qui-nexiste-pas:latest \
  -n production

# Créer un pod avec trop peu de mémoire (va crasher)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: oom-pod
  namespace: production
spec:
  containers:
    - name: memory-hog
      image: python:3.11-slim
      command: ["python3", "-c", "data = [' ' * 10**6 for _ in range(1000)]"]
      resources:
        limits:
          memory: 32Mi    # Trop peu → OOMKilled
EOF

# Créer un pod avec une sonde incorrecte
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: bad-probe-pod
  namespace: production
spec:
  containers:
    - name: app
      image: nginx:1.25
      readinessProbe:
        httpGet:
          path: /wrong-path
          port: 80
        initialDelaySeconds: 5
        periodSeconds: 5
EOF

E.2 Diagnostiquer les problèmes

# Voir tous les pods problématiques
kubectl get pods -n production \
  --field-selector='status.phase!=Running' 2>/dev/null || true

kubectl get pods -n production
# Observer les statuts : ImagePullBackOff, OOMKilled, CrashLoopBackOff

# Diagnostiquer bad-pod (ImagePullBackOff)
kubectl describe pod bad-pod -n production
# → Regarder la section "Events" : ErrImagePull

kubectl get events -n production \
  --field-selector involvedObject.name=bad-pod \
  --sort-by='.lastTimestamp'

# Diagnostiquer oom-pod (OOMKilled)
kubectl describe pod oom-pod -n production
# → "Last State: OOMKilled"
# → "Exit Code: 137" (signal SIGKILL)

kubectl get pod oom-pod -n production \
  -o jsonpath='{.status.containerStatuses[0].lastState}' | python3 -m json.tool

# Diagnostiquer bad-probe-pod (0/1 READY)
kubectl describe pod bad-probe-pod -n production
# → Readiness probe failed: HTTP probe failed with statuscode: 404

kubectl logs bad-probe-pod -n production
# Voir les logs nginx (accès à /wrong-path → 404)

E.3 Utiliser kubectl debug

# Déboguer bad-probe-pod avec un conteneur éphémère
kubectl debug -it bad-probe-pod \
  --image=curlimages/curl \
  --target=app \
  -n production \
  -- sh

# Dans le conteneur éphémère :
curl -v http://localhost:80/
curl -v http://localhost:80/wrong-path
exit

E.4 Dépannage réseau

# Créer un pod de debug réseau
kubectl run netdebug \
  --image=nicolaka/netshoot \
  --rm -it --restart=Never \
  -n production \
  -- bash

# Dans le pod de debug :
# Test DNS
nslookup log-generator.production.svc.cluster.local
nslookup kubernetes.default.svc.cluster.local

# Test connectivité
curl -s http://log-generator.production:80 || echo "Service non joignable"

# Voir les routes
ip route

# Test DNS externe
nslookup google.com

exit

E.5 Inspecter les ressources du cluster

# Utilisation CPU/Mémoire des pods
kubectl top pods -n production

# Utilisation par nœud
kubectl top nodes

# Voir les pods par consommation CPU
kubectl top pods -n production --sort-by=cpu

# Pods qui consomment le plus de mémoire
kubectl top pods --all-namespaces --sort-by=memory | head -10

# Vérifier les quotas de ressources
kubectl get resourcequota -n production
kubectl describe resourcequota -n production

# Vérifier les LimitRanges
kubectl get limitrange -n production

Partie F Dashboard Grafana

F.1 Importer des dashboards communautaires

Dans Grafana :

  1. Dashboards → Import
  2. Importer les IDs suivants :
    • 17781 — Kubernetes / Compute Resources / Namespace
    • 1860 — Node Exporter Full
    • 15141 — Kubernetes Cluster Monitoring

F.2 Créer un dashboard personnalisé

Créer un dashboard avec les panels suivants :

Panel 1 : Taux d'erreurs (LogQL)

rate({namespace="production"} | json | level="ERROR" [5m])

Panel 2 : Logs récents (table)

{namespace="production"} | json | level="ERROR" | line_format "{{.message}}"

Panel 3 : CPU pods (PromQL)

sum by (pod) (
  rate(container_cpu_usage_seconds_total{namespace="production"}[5m])
)

Panel 4 : Mémoire pods (PromQL)

sum by (pod) (
  container_memory_working_set_bytes{namespace="production", container!=""}
)

Nettoyage

# Supprimer l'application de test
kubectl delete namespace production

# Désinstaller la stack monitoring (optionnel)
helm uninstall prometheus-stack -n monitoring
helm uninstall loki -n monitoring
helm uninstall alloy -n monitoring
kubectl delete namespace monitoring

✅ Résultat attendu final

  • Stack Prometheus + Grafana + Loki opérationnelle
  • Alloy collectant les logs de tous les pods via DaemonSet
  • Dashboards Kubernetes dans Grafana
  • Requêtes LogQL maîtrisées
  • Alerte sur le taux d'erreurs configurée
  • Diagnostics kubectl sur des pods en erreur

🔧 Dépannage courant

Problème Solution
Loki ne reçoit pas de logs Vérifier les logs Alloy : kubectl logs -l app.kubernetes.io/name=alloy -n monitoring
Grafana affiche "No data" sur Loki Vérifier URL datasource + kubectl get svc loki-gateway -n monitoring
kubectl top affiche <unknown> Attendre 2-3 min pour metrics-server
Alerte reste "Normal" Vérifier la requête dans Explore d'abord
Pod OOMKilled redémarre en boucle Augmenter la limite mémoire