Applications complexes
Assembler les briques : un point d'entrée, des quotas, de l'élasticité.
Déploiement d’applications complexes
Vous savez maintenant écrire un Deployment, l’exposer, lui injecter sa configuration et lui donner un volume. Une application réelle, elle, se compose de plusieurs services qui doivent se parler, être joignables depuis Internet sous un seul nom de domaine, ne pas s’affamer mutuellement, et absorber une hausse de trafic sans intervention humaine.
Trois problèmes nouveaux apparaissent à cette échelle. Le premier est le point d’entrée : créer un Service de type LoadBalancer par microservice signifie autant d’adresses publiques facturées, sans routage par chemin ni certificat mutualisé. Le deuxième est le partage des ressources : sans contrainte déclarée, un service qui fuit en mémoire peut faire tomber des Pods voisins sur le même nœud. Le troisième est l’élasticité : dimensionner pour le pic revient à payer le pic en permanence.
Le quatrième problème n’est pas technique mais organisationnel, et c’est souvent celui qui pique le plus vite. Une application de cinq services représente une trentaine de manifestes, à dupliquer pour chaque environnement en changeant trois valeurs. Le copier-coller devient alors une source majeure de pannes : quelqu’un modifie le fichier de préproduction et oublie celui de production.
Cette leçon assemble les briques sur un exemple complet, puis répond à ce dernier point avec un aperçu de Helm. L’objectif n’est pas d’ajouter des concepts, mais de montrer comment ceux que vous connaissez tiennent ensemble.
L’application de référence
Trois composants dans un namespace dédié : une base de données PostgreSQL, une API, une interface web. Le namespace n’est pas décoratif — il porte les quotas, les règles d’accès, et permet de tout supprimer d’un geste.
apiVersion: v1
kind: Namespace
metadata:
name: boutique
labels:
environnement: production
La base de données
Un StatefulSet avec son Service headless, exactement selon le modèle de la leçon précédente : serviceName: postgres, clusterIP: None, un volumeClaimTemplates de 10 Gio. Deux ajouts propres à un déploiement complet, à insérer dans le conteneur :
# Une réservation généreuse : une base ordonnancée sur un nœud
# déjà chargé sera lente sans qu'aucune alerte ne se déclenche.
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
memory: "1Gi"
# pg_isready évite que l'API reçoive du trafic avant l'initialisation.
readinessProbe:
exec:
command: ["pg_isready", "-U", "boutique", "-d", "boutique"]
initialDelaySeconds: 10
periodSeconds: 5
L’API
C’est le manifeste le plus instructif : il rassemble configuration, secret, sondes et ressources.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: boutique
data:
APP_ENV: "production"
LOG_LEVEL: "info"
# Le nom du Service suffit : le DNS interne le résout dans le namespace.
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "boutique"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: boutique
labels:
app: api
tier: backend
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # la capacité ne baisse jamais pendant la bascule
maxSurge: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
tier: backend
spec:
containers:
- name: api
image: exemple/boutique-api:1.4.0
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: db-credentials
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "256Mi"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: boutique
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 8080
targetPort: 8080
name: http
L’interface web
Même structure : un Deployment à deux répliques et un Service ClusterIP sur le port 80. Un seul point réclame de l’attention, et il piège souvent :
env:
# Le navigateur n'est PAS dans le cluster : il ne résout pas
# http://api:8080. L'URL doit être un chemin public de l'Ingress.
- name: API_BASE_URL
value: "/api"
Seul le code exécuté dans le cluster bénéficie du DNS interne. Le JavaScript du navigateur, lui, passe obligatoirement par le point d’entrée public — d’où la section suivante.
L’Ingress
Un Ingress est une règle de routage HTTP : un nom de domaine, des chemins, des Services cibles. Un seul point d’entrée dessert toute l’application.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: boutique
namespace: boutique
annotations:
# Réécrit /api/produits en /produits avant transmission.
# Cette annotation est SPÉCIFIQUE à ingress-nginx.
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
tls:
- hosts:
- boutique.example.com
secretName: boutique-tls # Secret de type kubernetes.io/tls
rules:
- host: boutique.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Le point à comprendre avant de déboguer pendant une heure : l’objet Ingress ne fait rien à lui seul. C’est une déclaration inerte tant qu’un contrôleur d’entrée ne l’a pas lue. Sans contrôleur installé, apply réussit et rien ne route.
minikube addons enable ingress
kubectl get pods -n ingress-nginx # le contrôleur tourne-t-il ?
kubectl get ingress -n boutique # ADDRESS vide = règle non prise en charge
minikube ip # l'adresse à mettre dans /etc/hosts
Deuxième réserve, plus structurelle : les annotations sont propres à chaque contrôleur. nginx.ingress.kubernetes.io/rewrite-target n’a aucun sens pour Traefik, HAProxy ou l’AWS Load Balancer Controller, qui ont leur propre préfixe et leur propre syntaxe. Seule la structure rules est normalisée ; réécriture, délais, limitation de débit et redirection HTTPS sont du dialecte. C’est cette divergence qui a motivé la Gateway API, appelée à remplacer Ingress à terme.
Trois valeurs de pathType : Exact (correspondance stricte), Prefix (par segments de chemin, le cas courant) et ImplementationSpecific, délégué au contrôleur et requis pour les motifs à capture comme celui ci-dessus.
Requêtes et limites de ressources
Deux valeurs, deux rôles distincts. Les confondre produit des pannes difficiles à interpréter.
resources:
requests: # RÉSERVATION : sert à l'ordonnancement
cpu: "100m" # 100 millicores = 0,1 cœur
memory: "128Mi"
limits: # PLAFOND : sert à contenir les dérives
memory: "256Mi"
requests est ce que l’ordonnanceur utilise pour choisir un nœud : il additionne les requêtes des Pods déjà présents et ne place un nouveau Pod que si la somme reste sous la capacité. Un Pod sans requests est placé n’importe où, y compris sur un nœud déjà saturé.
limits est le plafond appliqué à l’exécution, et CPU et mémoire n’y réagissent pas du tout de la même façon :
- Dépasser la limite CPU provoque un throttling : le conteneur est ralenti. L’application devient lente, sans erreur ni redémarrage — symptôme sournois, souvent imputé à tort au réseau ou à la base de données.
- Dépasser la limite mémoire provoque un
OOMKilled: le conteneur est tué immédiatement. La mémoire n’est pas compressible.
kubectl describe pod api-7d4b8c9f5-xk2mp | grep -A5 "Last State"
kubectl top pods -n boutique # consommation réelle, si metrics-server est là
kubectl top nodes
D’où l’usage recommandé, contre-intuitif au premier abord : une limite mémoire, mais pas de limite CPU — c’est le choix fait dans les manifestes ci-dessus. La limite mémoire protège le nœud d’un voisin qui fuit, car une saturation mémoire provoque des évictions massives. La limite CPU, elle, laisse du processeur inutilisé alors qu’une application pourrait s’en servir : la requête garantit déjà sa part sous contention.
Le namespace peut imposer des garde-fous, plutôt que de dépendre de la discipline de chaque auteur de manifeste :
apiVersion: v1
kind: LimitRange
metadata:
name: valeurs-par-defaut
namespace: boutique
spec:
limits:
- type: Container
default: # limites appliquées si absentes du Pod
memory: "256Mi"
defaultRequest: # requêtes appliquées si absentes du Pod
cpu: "100m"
memory: "128Mi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-boutique
namespace: boutique
spec:
hard:
requests.cpu: "4"
requests.memory: "8Gi"
limits.memory: "16Gi"
persistentvolumeclaims: "10"
Conséquence à anticiper : dès qu’un ResourceQuota porte sur une ressource de calcul, tout Pod du namespace doit déclarer la valeur correspondante, sinon sa création est refusée. Le LimitRange est précisément ce qui évite que cette règle casse les déploiements existants.
L’autoscaling horizontal
Le HorizontalPodAutoscaler ajuste le nombre de répliques selon une métrique observée.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: boutique
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 70 % de la REQUÊTE, pas de la limite
behavior:
scaleUp: # monter vite pour absorber un pic
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown: # redescendre lentement, sinon ça oscille
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
Le détail décisif est dans le commentaire : averageUtilization: 70 porte sur la requête, pas sur la limite. Avec requests.cpu: 100m, la mise à l’échelle se déclenche à 70 millicores de consommation moyenne. Une requête mal calibrée rend donc l’autoscaling erratique : trop basse, il ajoute des répliques en permanence ; trop haute, il ne réagit jamais.
Deux prérequis stricts, qui expliquent la quasi-totalité des HPA qui « ne fonctionnent pas » : metrics-server doit être installé, et les conteneurs visés doivent déclarer requests pour la métrique choisie — sans requête CPU, le pourcentage est incalculable.
minikube addons enable metrics-server
kubectl get hpa -n boutique # TARGETS à <unknown> = métriques absentes
kubectl describe hpa api -n boutique
Notez enfin que le HPA ajoute des Pods mais ne crée pas de nœuds : si le cluster est plein, les nouveaux Pods restent en Pending. Ajouter de la capacité machine relève du Cluster Autoscaler ou de Karpenter, spécifiques au fournisseur cloud.
Déployer et vérifier l’ensemble
L’ordre de création n’a pas besoin d’être orchestré : les boucles de réconciliation convergent, et un Pod qui ne trouve pas encore sa dépendance sera relancé.
kubectl apply -f k8s/ --recursive
kubectl rollout status deployment/api -n boutique
kubectl rollout status statefulset/postgres -n boutique
kubectl get all -n boutique
kubectl get events -n boutique --sort-by=.metadata.creationTimestamp
Le diagnostic d’une application multi-services suit toujours le même chemin, de l’extérieur vers l’intérieur :
kubectl describe ingress boutique -n boutique # 1. la règle est-elle prise en charge ?
kubectl get endpoints -n boutique # 2. les Services pointent-ils sur des Pods ?
kubectl get pods -n boutique # 3. READY 3/3, pas seulement Running ?
kubectl port-forward -n boutique service/api 8080:8080 # 4. court-circuiter l'Ingress
Cette progression isole la panne en quatre commandes. Si port-forward fonctionne mais pas l’Ingress, le problème est dans le routage et non dans l’application — une heure de lecture de logs applicatifs économisée.
Un aperçu de Helm
Ces manifestes fonctionnent. Le problème arrive au deuxième environnement : la même chose avec deux répliques, un autre nom de domaine et une image de préproduction. Le copier-coller crée deux vérités qui divergeront.
Helm est un gestionnaire de paquets pour Kubernetes. Un chart regroupe des manifestes paramétrés dont les valeurs sont extraites dans un fichier séparé : Chart.yaml décrit le paquet, values.yaml porte les valeurs par défaut, et templates/ contient les manifestes.
# values.yaml
api:
image:
repository: exemple/boutique-api
tag: "1.4.0"
replicas: 3
ingress:
host: boutique.example.com
# templates/deployment-api.yaml — extrait
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.api.replicas }}
template:
spec:
containers:
- name: api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
# Vérifier le rendu SANS rien envoyer au cluster : à faire systématiquement
helm template boutique ./boutique -f values-production.yaml
helm install boutique ./boutique -n boutique --create-namespace
helm upgrade boutique ./boutique -n boutique -f values-production.yaml
helm history boutique -n boutique
helm rollback boutique 1 -n boutique
Ce qu’apporte Helm : un jeu de manifestes pour plusieurs environnements, un versionnement des livraisons, et un retour arrière à l’échelle de l’application entière — là où kubectl rollout undo ne traite qu’un objet à la fois.
Ce que cela coûte, et il faut le dire : la templatisation dégrade la lisibilité. Un chart très paramétré devient un exercice de logique de gabarit où l’on ne voit plus le YAML produit. C’est pourquoi des alternatives comme Kustomize, qui superpose des correctifs sur du YAML resté lisible, ont trouvé leur public. Prenez le réflexe de helm template avant tout helm upgrade : vous verrez ce que le cluster va réellement recevoir.
À retenir
- L’Ingress mutualise un point d’entrée HTTP pour tous vos Services. Il est inerte sans contrôleur d’entrée installé, et ses annotations sont propres à chaque contrôleur : seule la structure
rulesest portable. requestssert à l’ordonnancement,limitsà l’exécution. Dépasser la limite CPU ralentit silencieusement (throttling) ; dépasser la limite mémoire tue le conteneur (OOMKilled). D’où l’usage courant : toujours une limite mémoire, rarement une limite CPU.- Le HPA raisonne en pourcentage de la requête, pas de la limite. Il exige
metrics-serveret desrequestsdéclarées, et il ajoute des Pods sans créer de nœuds. - Diagnostiquez de l’extérieur vers l’intérieur : Ingress, puis endpoints, puis état des Pods, puis
port-forwardpour court-circuiter le routage et isoler la couche fautive. - Helm évite la duplication des manifestes entre environnements et apporte un retour arrière global. Le prix est la lisibilité : vérifiez toujours le rendu avec
helm templateavant d’appliquer.
Pour aller plus loin : les sujets qui prolongent naturellement ce cours sont le RBAC, les NetworkPolicy, l’observabilité (Prometheus, Grafana) et le déploiement continu par GitOps. La documentation officielle kubernetes.io/fr/docs/concepts/ reste la référence à consulter en premier.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion