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: ClusterIPkubectl 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: LoadBalancerkubectl 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=backendMé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 clusterkubectl 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"- Ouvrir l'URL dans le navigateur
- Configurer WordPress (titre, admin, mot de passe)
- Créer un article de test avec du contenu reconnaissable
- 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: 5Gikubectl 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}"
- Ouvrir dans le navigateur
- Configurer WordPress (langue, titre, admin)
- Créer un article de test avec du contenu
- 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: ClusterIPkubectl 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: NoSchedulehelm 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 :
- Connections → Data sources → Add data source
- Choisir Loki
- URL :
http://loki-gateway.monitoring.svc.cluster.local - 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
- Grafana → Explore (menu gauche, icône boussole)
- 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
- Grafana → Alerting → Alert rules → New alert rule
- 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 / TeamsPartie 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 productionPartie F Dashboard Grafana
F.1 Importer des dashboards communautaires
Dans Grafana :
- Dashboards → Import
- 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 |