Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Rendre une application conforme au 12-factor sur Kubernetes

Par mis à jour le

Améliorer cette page

Pour rendre une application conforme au 12-factor sur Kubernetes, on ne coche pas une liste : on mesure, facteur par facteur, ce que le cluster garantit et ce qu'il laisse à l'application. Cette leçon déploie podinfo et Redis sur un cluster kind, puis audite chaque facteur avec une commande et une sortie réelle : configuration injectée, Redis remplaçable par une variable, historique des révisions, Pod supprimé sans perte, arrêt propre, logs, Job d'administration. Elle montre aussi quatre contre-exemples mesurés et nomme les facteurs qu'aucun objet Kubernetes ne vérifie à votre place.

  • Associer chaque facteur à l'objet ou au réglage Kubernetes qui le porte, dans une grille d'audit.
  • Prouver qu'une configuration change sans toucher à l'image, et que Redis se remplace par une variable.
  • Lire l'historique d'un Deployment comme la trace des releases.
  • Supprimer un Pod pour vérifier que l'état vit ailleurs, et voir ce qui casse quand il vit sur le disque du Pod.
  • Mesurer l'effet de SIGTERM et de preStop sur un rolling restart.
  • Repérer les facteurs que Kubernetes ne vérifie pas seul.

La théorie des douze facteurs est dans la leçon Les principes du cloud native : ici, on ne la refait pas, on l'audite. Le principe sans état et le couplage faible sont aussi traités dans Loose coupling et stateless design.

Les objets Kubernetes utilisés sont supposés connus : Deployment, Service, ConfigMap, Secret, sondes, Job et rolling update. Il vous faut Docker, kind et kubectl en local (plus jq pour un seul filtre de logs). Toutes les durées et tous les compteurs de la leçon viennent d'exécutions réelles sur un poste de travail : ils montrent un ordre de grandeur, pas une valeur garantie. Les mesures viennent de kind v0.33.0, d'un nœud kindest/node:v1.37.0 et de kubectl v1.36.0, qui embarque kustomize v5.8.1.

Quels facteurs Kubernetes vérifie-t-il, et lesquels non ?

Section intitulée « Quels facteurs Kubernetes vérifie-t-il, et lesquels non ? »

Kubernetes porte la plupart des facteurs par un objet précis, mais il n'en vérifie aucun à votre place. Il applique ce que vous déclarez : si le disque du Pod contient l'état de l'application, il ne le détecte pas. La grille ci-dessous sert de plan d'audit. Chaque ligne donne l'objet ou le réglage, le geste qui prouve le facteur et le résultat mesuré dans le lab.

FacteurObjet ou réglage KubernetesPreuve observableRésultat mesuré
I. Base de codeaucun (dépôt Git)hors clusternon vérifié par Kubernetes
II. Dépendancesimage autonomedépendances embarquées dans l'image, aucune installation au démarragepodcli et curl viennent de l'image
III. ConfigurationConfigMap, Secret, envFrompatch puis rollout restartmessage changé en 2,95 s, image inchangée
IV. Services externesService redis, variable PODINFO_CACHE_SERVERpointer vers un second Redis404 sur l'autre Redis, 200 au retour
V. Build, release, runimage par digest, révisions du Deploymentrollout history, undo5 révisions, undo renumérote
VI. ProcessusPod sans volume, état dans Rediskubectl delete poddonnée intacte
VII. Associations de portsport nommé http, Servicewget http://podinfo/ depuis un Podl'application écoute elle-même sur 9898
VIII. Concurrencereplicaskubectl scale4 Pods Ready en moins de 2 s
IX. Jetablesondes, SIGTERM, preStopsuppression et rolling restart0 erreur avec preStop, 309 sans
X. Parité dev/prodkustomize base et overlaysdiff des deux rendus4 différences, toutes déclarées
XI. Logsstdout du conteneurkubectl logsune ligne JSON par événement
XII. AdministrationJob, même image, même configkubectl waitJob terminé en 4,1 s

Trois facteurs échappent au cluster. Le I (un dépôt par application) et le II (dépendances déclarées explicitement et isolées) se jugent dans le dépôt et le Dockerfile, pas dans kubectl. Le X est à moitié vérifiable : on compare ce qui est déployé, mais Kubernetes ne dit pas si votre Redis de production ressemble à celui du développement. Les sections suivantes montrent ces limites.

Le schéma montre l'application auditée. Regardez les deux sources de configuration qui entrent dans le Deployment, Redis qui reste en dehors comme ressource attachée, et le Job d'administration qui réutilise la même image et la même configuration.

Le Deployment podinfo reçoit une image par digest, un ConfigMap et un Secret par envFrom, expose un Service et appelle Redis par variable ; le Job admin-seed réutilise la même image et la même configuration.envFromenvFromPODINFO_CACHE_SERVERmême imagemême envFrommême envFromécrit la graineImage podinfo 6.15.0par digestConfigMappodinfo-configSecretpodinfo-secretDeployment podinfoService podinfoport 80 vers httpService redisressource attachéeJob admin-seed
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Le lab tient en un cluster kind et un répertoire de manifestes kustomize. La base contient un ConfigMap, un Secret, un Deployment podinfo, un Service, et un Deployment Redis avec son Service. Un overlay dev fixe le namespace twelve-factor et le message affiché. Les images sont épinglées par digest (l'empreinte SHA-256 du contenu de l'image, qui ne peut pas changer sous le même nom), comme sur tout le site.

Tous les fichiers de la leçon s'enregistrent dans un dossier de travail vide, rangé selon la convention kustomize (une base commune et des overlays qui la modifient) : créez-le avec mkdir -p base overlays/dev overlays/prod. Chaque bloc ci-dessous porte le chemin du fichier à créer.

Le ConfigMap porte les réglages non sensibles, dont l'adresse du cache. Le Secret porte une valeur de lab : un Secret est encodé en base64, pas chiffré. Par défaut, Kubernetes le stocke même sans chiffrement dans etcd (le chiffrement au repos s'active à part). Jamais de vraie valeur dans un exemple.

base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: podinfo-config
data:
PODINFO_UI_MESSAGE: "config v1 (base)"
PODINFO_UI_COLOR: "#34577c"
PODINFO_LEVEL: "info"
PODINFO_CACHE_SERVER: "tcp://redis:6379"
base/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: podinfo-secret
type: Opaque
stringData:
APP_API_TOKEN: "lab-token-non-secret-0001"

Le Deployment référence l'image par digest, injecte les deux objets avec envFrom, nomme son port, déclare deux sondes et prévoit une pause avant l'arrêt. Chaque champ sert un facteur, détaillé plus bas. Le preStop n'a été ajouté qu'au moment de mesurer le facteur IX : les mesures précédentes ne dépendent pas de lui.

base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
terminationGracePeriodSeconds: 30
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.15.0@sha256:ec73780a8425f59ea49f5bc8cdff0d598805a224fbaa1f86c67a244f250fa9da
command: ["./podinfo"]
ports:
- name: http
containerPort: 9898
envFrom:
- configMapRef:
name: podinfo-config
- secretRef:
name: podinfo-secret
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 1
periodSeconds: 2
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
# resources: requests et limits, omis ici

Le Service podinfo route le port 80 vers le port nommé http du Deployment.

base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
selector:
app: podinfo
ports:
- name: http
port: 80
targetPort: http

Le Deployment Redis (image redis:7.4.11-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499) est exposé par un Service redis sur le port 6379. Sa sonde de disponibilité exécute redis-cli ping.

base/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.4.11-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
ports:
- name: redis
containerPort: 6379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- name: redis
port: 6379
targetPort: redis

Deux fichiers kustomization.yaml assemblent le tout. La base liste les cinq fichiers ci-dessus. L'overlay dev reprend la base, fixe le namespace twelve-factor et remplace le message du ConfigMap par un patch JSON (une liste d'opérations replace sur un chemin précis).

base/kustomization.yaml
resources:
- configmap.yaml
- secret.yaml
- deployment.yaml
- service.yaml
- redis.yaml
overlays/dev/kustomization.yaml
namespace: twelve-factor
resources:
- ../../base
patches:
- target:
kind: ConfigMap
name: podinfo-config
patch: |-
- op: replace
path: /data/PODINFO_UI_MESSAGE
value: "config v1 (dev)"
  1. Créer le cluster avec l'image de nœud épinglée.

    Fenêtre de terminal
    kind create cluster --name cn-12f --image kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5

    Résultat attendu : Set kubectl context to "kind-cn-12f", en 9 à 11 s avec l'image du nœud déjà en cache local.

  2. Déployer l'overlay dev, après avoir créé le namespace twelve-factor (kustomize ne le crée pas) et positionné le contexte dessus.

    Fenêtre de terminal
    kubectl create namespace twelve-factor
    kubectl config set-context --current --namespace twelve-factor
    kubectl apply -k overlays/dev && kubectl rollout status deploy/redis deploy/podinfo
    configmap/podinfo-config created
    secret/podinfo-secret created
    service/podinfo created
    service/redis created
    deployment.apps/podinfo created
    deployment.apps/redis created
    deployment "redis" successfully rolled out
    deployment "podinfo" successfully rolled out

    Entre les lignes created et successfully rolled out, kubectl rollout status affiche des lignes d'attente (Waiting for deployment ...) tant que les Pods ne sont pas prêts. Images tirées comprises, l'ensemble a pris de 13 s à 27 s selon le débit de votre connexion.

  3. Interroger l'application par kubectl port-forward svc/podinfo 18080:80 dans un autre terminal.

    $ curl localhost:18080/readyz
    {
    "status": "OK"
    }
    $ curl localhost:18080/ | grep -E 'hostname|version|message'
    "hostname": "podinfo-75f75b6644-5gzjj",
    "version": "6.15.0",
    "message": "config v1 (dev)",

Le kubelet a bien tiré l'image par digest : le champ imageID du Pod vaut ghcr.io/stefanprodan/podinfo@sha256:ec73780a...fa9da. Un port-forward sur un Service reste lié à un seul Pod : relancez-le après chaque rollout, sinon curl vise un Pod disparu.

Le facteur III demande que la configuration vive dans l'environnement, pas dans l'image : sur Kubernetes, c'est le rôle du ConfigMap et du Secret injectés en variables. Le test qui prouve le facteur est de changer une valeur sans reconstruire quoi que ce soit. L'application lit ses réglages dans les variables d'environnement PODINFO_*, fournies par envFrom. Changez la valeur dans le ConfigMap, puis relisez le message servi par l'application.

Fenêtre de terminal
kubectl patch cm podinfo-config --type merge -p '{"data":{"PODINFO_UI_MESSAGE":"config v2 (dev)"}}'
curl localhost:18080/ | grep -E 'message|hostname'
configmap/podinfo-config patched
"hostname": "podinfo-75f75b6644-5gzjj",
"message": "config v1 (dev)",

Le message n'a pas bougé. Une variable d'environnement injectée par envFrom est figée à la création du conteneur : modifier le ConfigMap ne change pas un Pod déjà lancé. Il faut un nouveau Pod, que rollout restart produit.

Fenêtre de terminal
kubectl rollout restart deploy/podinfo && kubectl rollout status deploy/podinfo
curl localhost:18080/ | grep -E 'message|hostname'
deployment.apps/podinfo restarted
deployment "podinfo" successfully rolled out
"hostname": "podinfo-665c8c5946-d59wf",
"message": "config v2 (dev)",

Le rollout a pris 2,95 s, et ni l'image ni le code n'ont changé : c'est ce que le facteur III attend. Le Secret s'injecte de la même façon, et sa valeur apparaît dans le /env de podinfo : un Secret ne se commit jamais dans le dépôt avec une vraie valeur.

Comment déclencher le rollout seul quand la configuration change ?

Section intitulée « Comment déclencher le rollout seul quand la configuration change ? »

Un configMapGenerator de kustomize suffixe le nom du ConfigMap d'un hash de son contenu, ce qui relie la configuration à la révision du Deployment. Changer une valeur crée un nouveau ConfigMap, donc modifie le template du Deployment, donc déclenche un rollout sans autre geste. La variante testée vit dans un répertoire qui ne contient pas le ConfigMap de la base : elle reprend le Deployment, le Service et le Secret, et génère elle-même le ConfigMap. Elle s'installe dans son propre namespace, twelve-factor-gen. Ce détail compte : avec behavior: replace sur un ConfigMap hérité de la base, kustomize ne suffixe pas le nom d'un hash.

Fenêtre de terminal
mkdir -p variants/generator
cp base/deployment.yaml base/service.yaml base/secret.yaml variants/generator/
variants/generator/kustomization.yaml
namespace: twelve-factor-gen
resources:
- deployment.yaml
- service.yaml
- secret.yaml
configMapGenerator:
- name: podinfo-config
literals:
- PODINFO_UI_MESSAGE=config hash v1
- PODINFO_UI_COLOR=#34577c
- PODINFO_LEVEL=info

On applique une première fois, puis on change config hash v1 en config hash v2 dans le fichier (avec sed ou un éditeur) et on applique de nouveau. Les noms suffixés viennent du hash du contenu : vous obtiendrez les mêmes si vous reprenez ces valeurs à l'identique.

$ kubectl create namespace twelve-factor-gen
$ kubectl apply -k variants/generator ; kubectl get cm -n twelve-factor-gen
configmap/podinfo-config-64hg4ffmkh created
...
podinfo-config-64hg4ffmkh 3 1s
$ sed -i 's/config hash v1/config hash v2/' variants/generator/kustomization.yaml
$ kubectl apply -k variants/generator
configmap/podinfo-config-hbfbhm4t2f created
secret/podinfo-secret configured
service/podinfo unchanged
deployment.apps/podinfo configured
$ kubectl get cm -n twelve-factor-gen
podinfo-config-64hg4ffmkh 3 2s
podinfo-config-hbfbhm4t2f 3 1s

L'ancien ConfigMap est conservé, et c'est utile : kubectl rollout undo deploy/podinfo rétablit aussi l'ancienne configuration, puisque chaque révision pointe vers son ConfigMap. Pour le voir, lancez kubectl rollout undo deploy/podinfo -n twelve-factor-gen, puis listez le ConfigMap de chaque Pod avec kubectl get pod -n twelve-factor-gen -o jsonpath='{range .items[*]}{.metadata.name} {.spec.containers[0].envFrom[0].configMapRef.name}{"\n"}{end}' : pendant le retour arrière, un Pod encore sur le nouveau ConfigMap côtoie ceux de l'ancien, et une fois le retour terminé il ne reste qu'un Pod, sur l'ancien (podinfo-config-64hg4ffmkh).

Comment traiter Redis comme une ressource attachée ?

Section intitulée « Comment traiter Redis comme une ressource attachée ? »

Le facteur IV veut qu'un service externe soit une ressource que l'on remplace par un changement de configuration, jamais de code. Ici, l'application ne connaît de Redis que PODINFO_CACHE_SERVER=tcp://redis:6379, c'est-à-dire le nom DNS du Service redis. Le test : déployer un second Redis (redis-b, avec son Service), pointer l'application dessus par le ConfigMap, et observer.

Le second Redis reprend le manifeste du premier sous le nom redis-b. Enregistrez-le dans redis-b.yaml et déployez-le dans le namespace de la leçon.

redis-b.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-b
spec:
replicas: 1
selector:
matchLabels:
app: redis-b
template:
metadata:
labels:
app: redis-b
spec:
containers:
- name: redis-b
image: redis:7.4.11-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
ports:
- name: redis-b
containerPort: 6379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
periodSeconds: 2
---
apiVersion: v1
kind: Service
metadata:
name: redis-b
spec:
selector:
app: redis-b
ports:
- name: redis-b
port: 6379
targetPort: redis-b

Avant de changer de Redis, il faut une valeur à retrouver : on écrit ma-cle dans le premier Redis (le port-forward de l'étape 3 doit tourner). Puis on bascule le ConfigMap, on relance le port-forward (un rollout a remplacé le Pod) et on relit la clé.

Fenêtre de terminal
kubectl apply -f redis-b.yaml && kubectl rollout status deploy/redis-b
curl -X POST -d 'valeur-en-redis' localhost:18080/cache/ma-cle
curl localhost:18080/cache/ma-cle
kubectl patch cm podinfo-config --type merge -p '{"data":{"PODINFO_CACHE_SERVER":"tcp://redis-b:6379"}}'
kubectl rollout restart deploy/podinfo && kubectl rollout status deploy/podinfo
# relancez le port-forward : kubectl port-forward svc/podinfo 18080:80
curl -i localhost:18080/cache/ma-cle

La clé ma-cle rend valeur-en-redis avant le changement, puis répond 404 : l'application parle maintenant à un autre service, vide. Une écriture curl -X POST -d 'valeur-b' localhost:18080/cache/cle-b atterrit dans le second Redis : kubectl exec deploy/redis-b -- redis-cli keys '*' liste cle-b, alors que deploy/redis contient ma-cle. Dans les deux cas apparaît aussi une clé nommée d'après le Pod, que podinfo enregistre lui-même au démarrage : ignorez-la.

Après un retour à tcp://redis:6379 (même patch, même rollout restart), /cache/ma-cle rend de nouveau valeur-en-redis en 200, et /cache/cle-b rend 404. Aucun changement de code, d'image ni de Deployment : seulement une variable. Non testés ici : l'authentification, la persistance et la haute disponibilité de Redis.

Que montre l'historique du Deployment pour build, release et run ?

Section intitulée « Que montre l'historique du Deployment pour build, release et run ? »

Le facteur V sépare construire (l'image), publier une release (image plus configuration) et exécuter (les Pods). Sur Kubernetes, l'image est référencée par digest, et la release est le template du Deployment : chaque changement de template crée une révision. L'historique est donc la trace des releases, à condition de le documenter.

Pour tester, on change l'image pour la version précédente de podinfo (6.14.1, par digest) et on annote la cause du changement.

Fenêtre de terminal
kubectl set image deploy/podinfo podinfo=ghcr.io/stefanprodan/podinfo:6.14.1@sha256:f9537f729129d339aaef049b76ab2cd4ff06a424f99e5f6a5923c10621018fb1
kubectl annotate deploy/podinfo kubernetes.io/change-cause="release: podinfo 6.14.1 par digest" --overwrite
kubectl rollout history deploy/podinfo
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
4 <none>
5 release: podinfo 6.14.1 par digest

Les révisions 2, 3 et 4 sont des rollout restart (annotation restartedAt), pas des releases d'image : l'historique mélange tout, d'où l'intérêt de l'annotation kubernetes.io/change-cause. Après la bascule, curl localhost:18080/ donne "version": "6.14.1" et le message config v2 (dev) : la configuration a suivi, l'image a changé.

Fenêtre de terminal
kubectl rollout undo deploy/podinfo
kubectl get deploy podinfo -o jsonpath='{.spec.template.spec.containers[0].image}'
Warning: resource deployments/podinfo was previously managed with 'kubectl apply'. Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation...
deployment.apps/podinfo rolled back
ghcr.io/stefanprodan/podinfo:6.15.0@sha256:ec73780a8425...fa9da

L'avertissement dit que le Deployment a été créé par kubectl apply, qui garde sa dernière configuration dans une annotation ; le retour arrière la laisse périmée. Deux autres faits : undo renumérote (l'ancienne révision 4 devient la 6, sa cause est vide), et il n'annule pas un ConfigMap modifié par patch, puisque ce dernier n'est pas dans le template. Seul le ConfigMap nommé par hash revient avec l'undo.

# ❌ à éviter : changer la configuration par patch en croyant que rollout undo la restaure
$ kubectl patch cm podinfo-config --type merge -p '{"data":{"PODINFO_UI_MESSAGE":"config v2 (dev)"}}'
$ kubectl rollout undo deploy/podinfo # l'image revient, le message reste "config v2 (dev)"

Dernier piège : un kubectl apply -k overlays/dev ultérieur remet replicas à 1, la valeur de la base, et écrase un kubectl scale. Il ramène aussi le message du ConfigMap à config v1 (dev) : si vous le rejouez, refaites le patch vers config v2 (dev) avant les sections suivantes. Le dépôt reste la source de vérité de la release.

Les facteurs VI, VII et VIII se vérifient ensemble : un processus sans état, qui écoute lui-même sur un port, se multiplie par simple augmentation du nombre de Pods. Le test central est le plus brutal, supprimer un Pod et regarder si quelque chose se perd.

Si l'état vit dans un service attaché, supprimer le Pod ne perd rien. On écrit une valeur dans le cache, on la relit, puis on supprime le Pod qui l'a servie.

$ curl -X POST -d 'valeur-en-redis' localhost:18080/cache/ma-cle # [202]
$ kubectl delete pod -l app=podinfo # environ 9 s avec le preStop
$ kubectl get pods -l app=podinfo
podinfo-665c8c5946-2mspq 1/1 Running 0 5s
$ curl localhost:18080/cache/ma-cle
valeur-en-redis # [200]

Le Deployment a recréé un Pod de nom différent, et la valeur est toujours là, puisqu'elle vit dans Redis. Relancez le port-forward avant la dernière commande : celui d'avant visait le Pod supprimé. Limite : le Redis du lab n'a ni volume ni persistance, supprimer son Pod perdrait la donnée (non testé). C'est un cache, pas une source de vérité.

Quel est l'effet d'un état gardé sur le disque du Pod ?

Section intitulée « Quel est l'effet d'un état gardé sur le disque du Pod ? »

Un état écrit dans le système de fichiers du Pod disparaît avec lui. Le contre-exemple est volontairement fautif : configuration en dur dans les arguments, et un volume emptyDir monté sur /data (un volume vide créé avec le Pod et détruit avec lui).

etat-local.yaml
# ❌ à éviter : configuration en dur et état sur le disque local du Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo-local
spec:
replicas: 1
selector:
matchLabels:
app: podinfo-local
template:
metadata:
labels:
app: podinfo-local
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.15.0@sha256:ec73780a8425f59ea49f5bc8cdff0d598805a224fbaa1f86c67a244f250fa9da
command: ["./podinfo", "--ui-message=en dur dans args", "--data-path=/data"]
ports:
- name: http
containerPort: 9898
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: podinfo-local
spec:
selector:
app: podinfo-local
ports:
- name: http
port: 80
targetPort: http

Ce volume est indispensable : l'image 6.15.0 n'a pas de /data, et sans lui POST /store répond {"code":500,"message":"writing file failed"} (avec un code HTTP 200). Avec lui, l'écriture réussit, puis on supprime le Pod. Appliquez le fichier, attendez le déploiement, puis ouvrez un port-forward sur le port 18081 pour ne pas gêner celui de podinfo.

Fenêtre de terminal
kubectl apply -f etat-local.yaml && kubectl rollout status deploy/podinfo-local
kubectl port-forward svc/podinfo-local 18081:80 # dans un autre terminal
$ curl -w ' [%{http_code}]\n' -X POST -d 'etat-sur-disque-local' localhost:18081/store
{
"hash": "b0f16459feb69318ca4c6dca5ded6ab0c5d5c073"
} [202]
$ kubectl delete pod -l app=podinfo-local
$ kubectl port-forward svc/podinfo-local 18081:80 # relancé : le Pod a changé
$ curl -w ' [%{http_code}]\n' localhost:18081/store/b0f16459feb69318ca4c6dca5ded6ab0c5d5c073
{
"code": 500,
"message": "reading file failed"
} [200]

Le nombre entre crochets est le code HTTP affiché par curl -w : le 500 de la fin n'est que dans le corps de la réponse. Retirez ensuite le contre-exemple avec kubectl delete -f etat-local.yaml, avant de continuer.

La donnée a disparu avec le Pod. Deux erreurs se cumulent : l'état n'est pas dans un service attaché (facteur VI) et le message vient des arguments, si bien que modifier le ConfigMap ne l'atteindrait pas (facteur III). Piège de lecture : podinfo répond avec un code HTTP 200 ou 202 et un JSON qui porte "code": 500 : lisez le corps de la réponse, pas le code de curl -w.

Comment vérifier la liaison de port et la concurrence ?

Section intitulée « Comment vérifier la liaison de port et la concurrence ? »

Le facteur VII veut que l'application écoute elle-même sur son port, sans serveur web injecté ; le facteur VIII, qu'on la multiplie par le nombre de processus. Le port est nommé dans le Deployment, et le Service traduit le port 80 vers ce nom. Depuis un Pod client, l'accès se fait par le nom du Service.

Le Pod client est un busybox (busybox:1.37.0@sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e) lancé avec sleep 3600. On le crée d'abord, puis on attend qu'il soit prêt.

Fenêtre de terminal
kubectl run client --image=busybox:1.37.0@sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e -- sleep 3600
kubectl wait --for=condition=Ready pod/client --timeout=60s
kubectl get deploy podinfo -o jsonpath='{.spec.template.spec.containers[0].ports}'
kubectl exec client -- wget -qO- http://podinfo.twelve-factor.svc.cluster.local/readyz
[{"containerPort":9898,"name":"http","protocol":"TCP"}]
{
"status": "OK"
}

Pour lister les adresses derrière le Service, préférez kubectl get endpointslice : kubectl get endpoints affiche Warning: v1 Endpoints is deprecated in v1.33+.

Pour la concurrence, on passe à 4 réplicas et on envoie 20 requêtes.

Fenêtre de terminal
kubectl scale deploy/podinfo --replicas=4 && kubectl rollout status deploy/podinfo
for i in $(seq 1 20); do kubectl exec client -- wget -qO- http://podinfo/ | grep -oE 'podinfo-[a-z0-9]+-[a-z0-9]+'; done | sort | uniq -c
10 podinfo-66647cfb45-9dx87
1 podinfo-66647cfb45-bgcj6
4 podinfo-66647cfb45-zgz42
5 podinfo-66647cfb45-zrd4s

Les 4 Pods étaient Ready en moins de 2 s avec l'image déjà sur le nœud (la sonde de disponibilité attend 1 s), et les 4 reçoivent du trafic. La répartition n'est pas égale sur 20 requêtes, ce qui est normal. Lancée une seconde après le scale, la même boucle a envoyé 8 requêtes sur 8 au même Pod, le temps que le routage se propage : attendez quelques secondes. L'ajustement automatique du nombre de Pods n'est pas testé ici, il relève de l'autoscaling horizontal.

Kubernetes injecte aussi pour chaque Service des variables comme PODINFO_SERVICE_HOST. Comme le Service s'appelle podinfo et que podinfo lit le préfixe PODINFO_, les noms se croisent : rien n'a cassé ici (le port est resté 9898), mais c'est un piège de nommage. Le champ enableServiceLinks: false de la spec du Pod supprime ces variables (non testé ici).

Le facteur IX demande un démarrage rapide et un arrêt propre : Kubernetes détruit et recrée des Pods en permanence, l'application doit y survivre sans perdre de requête. Trois mesures le montrent : le démarrage, la réaction à SIGTERM, puis un rolling restart sous charge.

Le démarrage est mesuré par les horodatages Kubernetes : le Pod de remplacement, créé à 12:48:47Z au moment où l'ancien recevait SIGTERM, était Ready à 12:48:48Z, soit environ 1 s (image déjà sur le nœud, résolution à la seconde). À l'arrêt, kubectl delete pod envoie SIGTERM au processus principal ; podinfo ferme alors son serveur HTTP. Cette mesure a été faite avant l'ajout du preStop : avec le Deployment ci-dessus, la suppression dure 5 s de plus (rejouée : environ 4 s sans preStop, 9 à 11 s avec). Pour lire la ligne d'arrêt, suivez les logs du Pod dans un second terminal (kubectl logs -f <pod>) pendant que vous le supprimez.

SIGTERM envoyé vers 12:48:47.112
{"level":"info","ts":"2026-10-13T12:48:47.147Z","caller":"signals/shutdown.go:55","msg":"Shutting down HTTP/HTTPS server","timeout":5}
pod "podinfo-66647cfb45-zrd4s" deleted from twelve-factor namespace
durée suppression : 3275 ms

Une sonde posée toutes les 0,45 s sur l'IP du Pod pendant l'arrêt donne 503 sur /readyz dès la première sonde et pendant 3,3 s, puis la connexion est refusée : podinfo se déclare non prêt dès SIGTERM, patiente environ 3 s, puis ferme. Le contre-exemple est un processus qui ignore SIGTERM.

ignore-sigterm.yaml
# ❌ à éviter : PID 1 qui ignore SIGTERM (sleep), délai de grâce de 10 s
apiVersion: v1
kind: Pod
metadata:
name: ignore-sigterm
spec:
terminationGracePeriodSeconds: 10
containers:
- name: sleeper
image: busybox:1.37.0@sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e
command: ["sleep", "3600"]

Enregistrez ce Pod dans ignore-sigterm.yaml, appliquez-le, attendez qu'il soit prêt, puis mesurez sa suppression avec time.

$ kubectl apply -f ignore-sigterm.yaml && kubectl wait --for=condition=Ready pod/ignore-sigterm
$ time kubectl delete pod ignore-sigterm
pod "ignore-sigterm" deleted from twelve-factor namespace
(durée relevée par time : 11,4 s)

La suppression a duré 11,4 s : le kubelet attend la fin du terminationGracePeriodSeconds (10 s), puis envoie SIGKILL. Un processus sourd à SIGTERM rend chaque rollout aussi long que le délai de grâce, et coupe les requêtes en cours.

Pourquoi ajouter un preStop pendant un rolling restart ?

Section intitulée « Pourquoi ajouter un preStop pendant un rolling restart ? »

À la fin de vie d'un Pod, le retrait de ses adresses du Service et l'envoi de SIGTERM partent en parallèle : des requêtes peuvent encore arriver sur un Pod qui répond déjà 503. Le crochet preStop s'exécute avant SIGTERM et le retarde de quelques secondes, le temps que le retrait se propage. Le délai de grâce (terminationGracePeriodSeconds: 30) court dès le début du preStop et l'inclut : preStop plus arrêt normal doivent tenir dans ces 30 s. Pour le mesurer, une boucle wget /healthz tourne 25 s depuis le Pod client, sur 3 réplicas, avec un rollout restart lancé à t+2 s. Pour la ligne « sans preStop », il suffit de retirer le bloc lifecycle du Deployment.

Fenêtre de terminal
kubectl scale deploy/podinfo --replicas=3 && kubectl rollout status deploy/podinfo
(sleep 2; kubectl rollout restart deploy/podinfo) &
kubectl exec client -- sh -c 'ok=0; ko=0; end=$(( $(date +%s) + 25 )); while [ $(date +%s) -lt $end ]; do if wget -qO- -T 2 http://podinfo/healthz >/dev/null 2>&1; then ok=$((ok+1)); else ko=$((ko+1)); fi; done; echo "ok=$ok erreurs=$ko"'

La boucle tourne dans le Pod client pendant 25 s et affiche le nombre de réponses réussies et d'erreurs. Pour retirer le preStop, kubectl patch deploy podinfo --type json -p '[{"op":"remove","path":"/spec/template/spec/containers/0/lifecycle"}]' suffit ; kubectl apply -k overlays/dev le remet. Rejoué ici, le même scénario a donné erreurs=0 avec le preStop (deux essais) et de 300 à 352 erreurs sans (deux essais) : les totaux de requêtes dépendent de la machine, l'ordre de grandeur reste le même.

ConfigurationRequêtes OKErreurs
sans preStop8 738309 (3,4 %)
avec lifecycle.preStop: sleep 59 4120

Sans pause, 3,4 % des requêtes ont échoué pendant le rollout ; avec cinq secondes de sleep, aucune. C'est une exécution par ligne, sans statistique : le résultat montre le mécanisme et un ordre de grandeur, pas un taux garanti. Autre piège : kubectl logs deploy/podinfo pendant un rollout peut tomber sur le Pod qui s'arrête (Found 2 pods, using pod/...), préférez -l app=podinfo.

Le facteur XI veut que l'application écrive ses événements sur la sortie standard, et laisse la plateforme les collecter. Podinfo écrit une ligne JSON par événement sur stdout, aucun fichier. kubectl logs les lit directement.

Fenêtre de terminal
kubectl logs -l app=podinfo --tail=20
{"level":"info","ts":"2026-10-13T12:53:02.747Z","caller":"podinfo/main.go:170","msg":"Starting podinfo","version":"6.15.0","revision":"dd507173...","port":"9898"}
{"level":"info","ts":"...","caller":"http/server.go:273","msg":"Starting HTTP Server.","addr":":9898"}

Pour diagnostiquer un Pod au-delà de ces lignes, voir Débugger une application Kubernetes. Au niveau info, les requêtes ne sont pas journalisées. Passer à PODINFO_LEVEL=debug demande un changement de configuration et un rollout, ce qui rejoue le facteur III : une ligne JSON par requête apparaît, avec par exemple "uri":"/readyz" et l'agent kube-probe/1.37. Un filtre suffit : kubectl logs -l app=podinfo --tail=50 | jq -r .level | sort | uniq -c a donné 15 debug et 2 info. L'acheminement vers un système de collecte n'est pas testé : l'application ne s'en occupe pas, c'est le contrat du facteur.

Le facteur XII veut que les tâches ponctuelles (migration, graine de données) tournent comme des processus à part, avec le même code et la même configuration que l'application. Sur Kubernetes, c'est un Job. Celui du lab réutilise l'image par digest, le même envFrom, et les outils podcli et curl fournis par l'image. Il écrit une valeur dans le cache. Enregistrez-le dans job-admin.yaml.

job-admin.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: admin-seed
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
containers:
- name: admin
image: ghcr.io/stefanprodan/podinfo:6.15.0@sha256:ec73780a8425f59ea49f5bc8cdff0d598805a224fbaa1f86c67a244f250fa9da
envFrom:
- configMapRef:
name: podinfo-config
- secretRef:
name: podinfo-secret
command:
- sh
- -c
- |
set -e
echo "config vue par le Job: message=$PODINFO_UI_MESSAGE cache=$PODINFO_CACHE_SERVER token=${#APP_API_TOKEN} caracteres"
podcli check tcp "${PODINFO_CACHE_SERVER#tcp://}"
curl -fsS -X POST -d "graine-par-le-job" http://podinfo/cache/seed
echo "graine ecrite"

Le script affiche la longueur du secret, jamais sa valeur. ttlSecondsAfterFinished: 600 fait nettoyer le Job dix minutes après sa fin. Il n'appartient pas à la base kustomize, parce qu'un Job est immuable.

$ kubectl apply -f job-admin.yaml && kubectl wait --for=condition=complete job/admin-seed --timeout=90s
job.batch/admin-seed created
job.batch/admin-seed condition met
$ kubectl logs job/admin-seed
config vue par le Job: message=config v2 (dev) cache=tcp://redis:6379 token=25 caracteres
... check succeed {"address": "redis:6379"}
graine ecrite
$ kubectl exec client -- wget -qO- http://podinfo/cache/seed
graine-par-le-job

Le Job a vu la même configuration que le Deployment, atteint le même Redis et écrit une valeur que l'application relit : kubectl get job admin-seed indique Complete 1/1 en 4 s. À éviter, sans l'avoir mesuré ici : lancer la même tâche à la main avec kubectl exec dans un Pod en cours d'exécution, car elle n'est ni tracée, ni rejouable, et dépend de l'état de ce Pod.

Le facteur X demande de garder développement et production aussi proches que possible : même artefact, configuration différente. Ici, une base commune et deux overlays kustomize produisent deux namespaces. Le contrôle est un diff des deux rendus. L'overlay prod ressemble à dev : même base, autre namespace, et trois valeurs changées (niveau de log, message, nombre de réplicas).

overlays/prod/kustomization.yaml
namespace: twelve-factor-prod
resources:
- ../../base
patches:
- target:
kind: ConfigMap
name: podinfo-config
patch: |-
- op: replace
path: /data/PODINFO_UI_MESSAGE
value: "config v1 (prod)"
- op: replace
path: /data/PODINFO_LEVEL
value: "warn"
- target:
kind: Deployment
name: podinfo
patch: |-
- op: replace
path: /spec/replicas
value: 2

Avant de comparer, déployez prod (il faut créer son namespace comme pour dev) : kubectl create namespace twelve-factor-prod && kubectl apply -k overlays/prod. Le diff rend un code de sortie 1 quand les rendus diffèrent : c'est le résultat attendu ici.

Fenêtre de terminal
diff <(kubectl kustomize overlays/dev) <(kubectl kustomize overlays/prod)
kubectl get deploy -A -o 'custom-columns=NS:.metadata.namespace,NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image'

Le diff ne montre que le niveau de log, le message, le namespace et replicas: 1 contre 2. Les deux Deployments podinfo portent la même image par digest (ghcr.io/stefanprodan/podinfo:6.15.0@sha256:ec73780a...fa9da), et Redis est identique. Dans twelve-factor-prod, kubectl get pods liste 2 Pods podinfo et 1 Redis.

Ce test compare des manifestes, pas des environnements réels. Kubernetes ne dit pas si le Redis de production a la même version, la même persistance ou la même charge que celui de développement. Le facteur X reste une discipline d'équipe, que l'outil ne rend vérifiable que sur les objets déclarés.

Ces symptômes sont ceux rencontrés pendant le lab.

SymptômeCauseSolution
Le message de l'application ne change pas après kubectl patch cmLes variables injectées par envFrom sont figées à la création du conteneurkubectl rollout restart deploy/podinfo, ou un ConfigMap nommé par hash
curl ne répond plus après un rolloutUn port-forward svc/... est lié à un seul Pod, disparuRelancer le port-forward
rollout undo laisse la nouvelle configurationLe ConfigMap modifié par patch n'est pas dans le templateUtiliser un configMapGenerator
kubectl apply -k ramène le nombre de réplicas à 1La valeur de la base écrase le kubectl scaleDéclarer replicas dans l'overlay
8 requêtes sur 8 vont au même Pod juste après scaleLa table de routage n'est pas encore propagéeAttendre quelques secondes avant de mesurer
Des erreurs 503 apparaissent pendant un rolling restartRetrait des adresses et SIGTERM partent en parallèleAjouter lifecycle.preStop (sleep 5 dans le lab)
La suppression d'un Pod dure exactement le délai de grâceLe processus principal ignore SIGTERM, le kubelet attend puis envoie SIGKILLFaire gérer SIGTERM par le processus 1
/store répond 200 avec un corps "code": 500podinfo renvoie l'erreur dans le JSONLire le corps de la réponse, pas le code HTTP
Un générateur kustomize ne suffixe pas le ConfigMapbehavior: replace sur un ConfigMap de la base ne produit pas de hashUn répertoire qui ne contient pas le ConfigMap de la base
kubectl logs deploy/podinfo donne les logs d'un Pod qui s'arrêteLe Deployment a plusieurs Pods pendant le rolloutkubectl logs -l app=podinfo ou nommer le Pod

Le cluster kind est jetable : le supprimer retire tous les objets du lab.

Fenêtre de terminal
kind delete cluster --name cn-12f
Deleting cluster "cn-12f" ...
Deleted nodes: ["cn-12f-control-plane"]
  • Kubernetes porte les facteurs par des objets (ConfigMap, Secret, Service, Job, sondes) mais ne vérifie rien : c'est à vous de prouver chaque facteur par une commande.
  • Les facteurs I et II se jugent dans le dépôt et l'image, pas dans le cluster ; le X ne se vérifie que sur les manifestes.
  • Une variable injectée par envFrom est figée à la création du conteneur : changer le ConfigMap demande un nouveau Pod, et un nom de ConfigMap suffixé d'un hash relie configuration et révision.
  • Un service externe est une ressource attachée quand un changement de variable suffit à le remplacer.
  • Un Pod doit pouvoir être supprimé sans perte : l'état vit dans un service attaché, jamais sur le disque du Pod.
  • Un processus qui ignore SIGTERM rend la suppression aussi longue que le délai de grâce ; un preStop évite des erreurs pendant le retrait du Pod du Service (309 erreurs sur 9 047 requêtes sans, 0 avec, une exécution par cas).
  • Un Job qui réutilise l'image et la configuration de l'application est la forme Kubernetes d'une tâche d'administration.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn