Case study · migration K8s overnight

La nuit où on est passé de Swarm à K8s

13 mai 2026, 22h. Confirmation : la boîte ferme, recherche d'emploi en cours. Le portfolio doit rester debout quand un recruteur clique. 14 mai, 00h30 : 5 apps en K8s, 5 ArgoCD Applications Synced + Healthy, 1 repo GitOps public. ~24 commits, ~10 builds Docker, 0 ligne de code humaine. Voici le récit, les pièges, et les leçons.

Le déclencheur

Le 13 mai 2026 vers 18h, ma boîte annonce qu'elle ferme. Dans les 4 heures qui suivent, ma préoccupation passe de "je continue les sprints maritime" à "mon portfolio doit fonctionner quand un recruteur clique".

Le portfolio en question : 5 apps personnelles déployées sur un NAS Synology via Docker Swarm. Quand le NAS rame (Btrfs scrub post-coupure secteur, comme ce jour-là), tout tombe en cascade : l'ais-decoder maritime sature le WAL Postgres, Postgres ne peut plus servir GeoServer qui ne peut plus servir le frontend. Le NAS est devenu un SPoF inacceptable pour une période où chaque clic compte.

La décision est prise vers 22h : migration K8s, plan local-first (cluster k3s sur Big-Blue, mon poste WSL2), puis phase 2 vers Scaleway Kapsule quand tout sera stable. Objectif explicite : maximiser la vendabilité CV (Kubernetes, GitOps, operators, cloud-native) tout en gardant un portfolio publiquement accessible.

Avant / Après

Avant — Docker Swarm sur NAS

  • · 1 NAS Synology Btrfs
  • · Docker Swarm 20 services dont 14 maritime
  • · 5 sites Docker compose séparés
  • · Pas d'autoscaling natif
  • · 1 volume Btrfs partagé par tous (Postgres, GeoServer, RMQ)
  • · Pas de GitOps — `docker stack deploy` manuel
  • · TLS via Cloudflare tunnel (1 tunnel par site)
  • · Boîte qui ferme = portfolio inaccessible quand recruteur clique

Après — k3s + GitOps

  • · 1 cluster k3s sur Big-Blue (WSL2 Ubuntu, i9-14900KF, 31 GiB)
  • · 5 apps en K8s (ol, finance, warhammer, maritime, geoserver)
  • · 1 repo GitOps public `Sylad/developpeur-gitops`
  • · 5 ArgoCD Applications Synced + Healthy
  • · PVC SSD dédié par stateful workload
  • · ingress-nginx + cert-manager + mkcert TLS dual (WSL + Windows)
  • · 5 operators K8s : CloudNativePG, RabbitMQ Cluster Operator, KEDA, cert-manager, sealed-secrets
  • · Plan Phase 2 : Scaleway Kapsule (~€47/mo) prêt à activer

Architecture cible

Plan en deux phases : valider toute l'archi en local sur Big-Blue (€0, itération à la seconde), puis pousser sur Scaleway Kapsule managé (~€47/mois) seulement quand tout est stable. C'est le workflow standard "Platform Engineering / Internal Developer Platform" qu'utilisent les équipes mid-sized en 2026.

Architecture
Loading diagram…

Le NAS ne disparaît pas — il devient un edge collector qui pousse les données AIS live vers RabbitMQ dans le cluster via TLS sortant. Pattern reconnu (IoT edge / hybrid cloud) également vendable sur CV.

La nuit chronologique

Découpage en 5 sprints, livrés dans cet ordre :

Sprint 0 — Bootstrap cluster · 22h00 → 22h45

k3s install + helm + mkcert. Création 3 namespaces (preprod, argocd, infra). Install ingress-nginx (hostNetwork=true — le piège #1 ci-dessous a coûté 30 min de debug). cert-manager + ClusterIssuer basé sur la CA mkcert. ArgoCD via Helm chart + Ingress TLS auto-signé reconnu par Chrome Windows (avec certutil -addstore du root CA mkcert côté Windows).

Sprint 1 — ol-companion · 22h45 → 23h15

La plus simple des apps personnelles. Premier Helm chart générique (backend NestJS + frontend nginx + Ingress + PVC). Smoke test https://ol.dev.local → HTTP/2 200, cadenas vert dans Chrome. Le pattern Helm chart est défini, les suivantes vont être des copies.

Sprint 2 — finance + warhammer · 23h15 → 23h45

Cherry-pick du chart ol-companion. Build des 4 images en parallèle (2 backends + 2 frontends). Sed pour ajuster ports/hostnames/env. Warhammer-backend crash sur seeds JSON manquants — pattern "helper pod busybox monte le PVC + kubectl cp + restart app" pour pré-populer (1.6 MB seeds). 2 nouvelles URLs HTTPS en ligne.

Maritime stateful · 23h45 → 00h05

Install 3 operators : CloudNativePG (Postgres HA), RabbitMQ Cluster Operator, KEDA (event-driven autoscaling, remplace mon autoscaler maison Swarm). Création de 2 clusters PG distincts (dual-DB pattern) : pg-catalog (tiny, GeoServer JDBCConfig) et pg-data (hypertables, PostGIS). Dump du schema geoserver depuis le NAS (175 rows) → restore dans pg-catalog via psql stdin pipe.

Maritime stateless · 00h05 → 00h20

Build des 5 images stateless (api, ais-decoder, alerts-engine, track-builder, frontend Angular). Helm chart umbrella avec _helpers.tpl qui compose DATABASE_URL depuis le Secret CNPG pg-data-app et RABBITMQ_URL depuis le Secret RMQ Operator rmq-default-user. Smoke https://maritime.dev.local → HTTP/2 200.

GeoServer · 00h20 → 00h35

Le plus tordu. Build image custom GeoServer 2.28.3 + plugins (WPS, control-flow, JDBCConfig, JDBCStore, gwc-s3) + JAR plugin maison IDW via Dockerfile multi-stage. Dump du data_dir NAS (73 MB) → restore dans PVC via helper pod. Patch jdbcconfig.properties in-place (hostname Swarm → CNPG). Disable JDBCStore et le bloc <S3BlobStore> de gwc/geowebcache.xml. Ingress dédié geoserver.dev.local avec sticky session cookie + cert TLS. Pod 0/1 Ready pendant quelques minutes (le piège #5 — bon endpoint healthcheck), puis 200.

GitOps closure · 00h35

gh repo create developpeur-gitops --public + push initial du chart ol-companion. Création de 5 ArgoCD Applications (ol, finance, warhammer, maritime-stateful, maritime) qui sync depuis ce repo. État final : 5/5 Synced + Healthy. 6 URLs publiques HTTPS qui répondent 200.

Les 7 pièges majeurs (et leurs fixes)

Ce qui rend ce projet vendable sur CV : ce ne sont pas les commandes qui ont marché du premier coup, mais les debugs qui ont coûté 15-30 min chacun et qu'on a documentés.

1.

ingress-nginx en mode classique ne forwarde rien depuis Windows

Cause : K3s utilise Klipper LB qui fait du DNAT iptables — il n'y a PAS de processus qui LISTEN sur l'IP WSL2. Windows ne détecte rien à forwarder via localhost.

Fix : helm upgrade ingress-nginx --set controller.hostNetwork=true --set controller.dnsPolicy=ClusterFirstWithHostNet --set controller.kind=DaemonSet. Le pod bind alors sur 0.0.0.0:80/443 → Windows auto-forwarde.

2.

CloudNativePG webhook impose un tag image semver strict

Cause : spec.imageName: Invalid value: 'timescale/timescaledb-ha:pg16-ts2.17': invalid version tag. La webhook attend N.N.N strict.

Fix : docker tag local avant import : `docker tag timescale/timescaledb-ha:pg16-ts2.17 maritime-pg:16.0.0`. Le tag 16.0.0 passe la webhook.

3.

CNPG bloque shared_preload_libraries dans postgresql.parameters

Cause : spec.postgresql.parameters.shared_preload_libraries: Can't set fixed configuration parameter. CNPG le gère lui-même (pgaudit, pg_failover_slots).

Fix : Utiliser le champ list top-level `spec.postgresql.shared_preload_libraries: [timescaledb]` qui MERGE avec ceux de CNPG.

4.

CNPG postInitSQL casse les blocs plpgsql `$$ ... $$`

Cause : CNPG split le YAML array sur `;`. Les blocs `BEGIN ... END;` plpgsql ont leur propre `;` interne → syntax error at or near '$'.

Fix : Utiliser `LANGUAGE sql` inline ('SELECT NULL::void') quand possible. Sinon, exécuter le plpgsql via `kubectl exec | psql` post-bootstrap.

5.

GeoServer healthcheck — le bon endpoint n'est pas /geoserver/web/

Cause : /geoserver/web/ = 302 (redirect). /geoserver/web/index.html = 404. Seul /geoserver/index.html retourne 200 strict.

Fix : startupProbe.httpGet.path: /geoserver/index.html. failureThreshold: 60 × 15s = 15 min de grâce pour le boot Java + Spring + JDBCConfig.

6.

Le jdbcconfig.properties du data_dir surcharge les env vars

Cause : GS lit d'abord le fichier disque (qui contient encore l'ancien hostname Swarm 'postgres:5432'). Les env GEOSERVER_JDBCCONFIG_JDBC_URL sont ignorés.

Fix : sed direct dans le PVC via pod helper busybox : sed -i 's|postgres:5432|pg-catalog-rw:5432|g' jdbcconfig.properties.

7.

ArgoCD selfHeal revert tout mon helm upgrade local en 3 min

Cause : Application avec syncPolicy.automated.selfHeal: true compare l'état cluster vs Git toutes les 3 min. `kubectl patch` / `helm upgrade --force` direct sont reverts garantis.

Fix : Workflow safe = commit + git push d'abord, puis ArgoCD sync. Jamais de modif manuelle sur les ressources gérées par ArgoCD (sauf si selfHeal disabled explicitement).

Ces 7 pièges sont sauvés en mémoire long-terme dans le repo gitops (et également dans la mémoire de Claude Code pour les futures sessions). Format : un fichier markdown par piège, avec cause + fix + références croisées via wikilinks [[autre-piège]].

Capture portfolio · ArgoCD UI

L'ArgoCD UI est le visuel central qui résume la nuit. 5 Applications listées, toutes en Synced + Healthy sauf maritime qui montre Progressing à la capture (le pod GeoServer terminait son boot Java). Cliquer sur n'importe quelle card ouvre l'arborescence des ressources gérées (Deployments, Services, Ingress, PVC, Certificate) en arbre cliquable avec leur état temps réel.

ArgoCD UI montrant les 5 Applications du repo developpeur-gitops après la migration nocturne
ArgoCD Applications Tiles — finance-tracker, maritime, maritime-stateful, ol-companion, warhammer40k. Sync = Synced (config matche Git), Health = Healthy / Progressing.

Smoke tests terminal

Les 3 commandes que je voulais voir vertes à la fin de la nuit :

$ kubectl get app -n argocd -o custom-columns='NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status'

NAME                SYNC     HEALTH
finance-tracker     Synced   Healthy
maritime            Synced   Healthy
maritime-stateful   Synced   Healthy
ol-companion        Synced   Healthy
warhammer40k        Synced   Healthy

$ kubectl get pods -n maritime

NAME                                READY   STATUS    RESTARTS   AGE
ais-decoder-6549dc648c-j7p56        1/1     Running   0          51m
alerts-engine-b94cb44f9-zlwwf       1/1     Running   0          20m
geoserver-69c645c55d-rh7tn          1/1     Running   0          5m
maritime-api-648898b545-xh4zd       1/1     Running   0          19m
maritime-frontend-9f9ffcff8-5pfx6   1/1     Running   0          20m
pg-catalog-1                        1/1     Running   0          95m
pg-data-1                           1/1     Running   0          10m
rmq-server-0                        1/1     Running   0          95m
track-builder-856676fd85-l9gl6      1/1     Running   0          20m

$ for u in ol finance warhammer maritime argocd ; do curl -sI -L "https://$u.dev.local" | head -1 ; done

HTTP/2 200
HTTP/2 200
HTTP/2 200
HTTP/2 200
HTTP/2 200

$ curl -sI -L https://geoserver.dev.local/geoserver/index.html | head -1

HTTP/2 200

6 URLs HTTPS, toutes 200, toutes avec cadenas vert dans Chrome Windows (cert auto-généré par cert-manager + ClusterIssuer CA basé sur mkcert, root CA importé dans le trust store Windows via certutil -addstore -f ROOT).

Bilan technique

Composant État
k3s single-node Big-Blue WSL2v1.35.4 ✓
ingress-nginx (hostNetwork)Running, Windows forward OK
cert-manager + mkcert ClusterIssuerSigning CA verified
ArgoCD UIhttps://argocd.dev.local 200
sealed-secrets controllerRunning (à câbler weekend)
CloudNativePG operatorRunning, 2 Cluster CR healthy
RabbitMQ Cluster OperatorRunning, 1 Cluster CR ready
KEDA operatorInstalled (ScaledObject à câbler weekend)
5 ArgoCD ApplicationsAll Synced + Healthy
6 URLs HTTPS dev.localToutes 200, cadenas vert Chrome
Repo public Sylad/developpeur-gitops3 helm charts + 5 Application manifests

Les leçons à reprendre

1. K8s local-first vs cloud direct

Commencer par k3s sur poste de dev WSL2 économise 6h de bootstrap cloud + €40/mo pour la phase d'apprentissage. Le cluster local est suffisant pour valider 100% de l'archi (operators, GitOps, observability). Le cloud devient un kubectl apply de la même config quand on veut publier.

2. Le dual-DB pattern n'est pas optionnel

Quand on a 2 workloads PostgreSQL incompatibles (catalog GeoServer tiny+read-mostly vs hypertables maritime append-only high-volume), 2 clusters CNPG distincts (pg-catalog + pg-data) = 2 WAL isolés. Le crash overnight 2026-05-13 où l'ais-decoder saturait le WAL et empêchait GeoServer de lire 175 rows de catalog était évitable avec ce pattern.

3. GitOps + selfHeal = source of truth = Git

Une fois ArgoCD branché avec selfHeal, tout `kubectl patch` direct est reverté. Ça force une discipline saine : tout changement passe par git commit + push. Mauvaise nouvelle pour le debug rapide, mais excellent pour l'audit, le rollback, et la reproductibilité.

4. Les images custom sont normales en K8s sérieux

On a buildé 3 images custom pendant la nuit (maritime-pg-ts pour Timescale UID 26, maritime-geoserver:v2 pour le plugin IDW, maritime-pg-ts:16.0.0 pour le retag CNPG-compliant). C'est la signature d'un déploiement sérieux — on ne se contente plus d'images publiques génériques.

5. Trade-offs assumés > perfection bloquée

Timescale stubs no-op au lieu de l'extension réelle. GWC S3 disabled au lieu de déployer SeaweedFS. JDBCStore désactivé pour preprod 1 replica. Ces trade-offs ne sont PAS des défauts du déploiement — ils sont documentés, scopés à la preprod, avec un plan de fix pour la prod. Le senior assume, le junior cache.

Trade-offs assumés (preprod uniquement)

Cette migration est préprod local, pas prod publique. Trois compromis explicites pour rester scopés à la nuit, tous documentés avec un plan de fix :

Trade-off Pourquoi Plan
pg-data sans Timescale réel
(stubs SQL no-op)
L'image custom maritime-pg-ts:16.0.0 (UID 26 + Timescale apt) plante en CNPG (`could not access file timescaledb`) alors qu'elle marche en standalone. Piège CNPG-specific à debugger. Phase 6 weekend
GWC S3 disabled
(bloc <S3BlobStore> retiré)
SeaweedFS pas déployé en K8s. Service stub sans backend → 502 au boot GeoServer. Phase 7 weekend : déployer SeaweedFS Helm chart + ré-activer S3
JDBCStore disabled
(enabled=false)
JDBCStore persiste les fichiers config en blobs DB (gwc/geowebcache.xml inclus, avec hostname S3 hardcoded). Patcher le fichier disk inutile tant que JDBCStore lit depuis DB. Réactiver Phase 7 quand SeaweedFS up

Ces trade-offs sont la signature du senior : on assume, on document, on planifie. Le junior cache, le senior trace.

Et après ?

Roadmap pour les semaines à venir (par ordre d'effort) :

  • Phase 6 — Timescale custom image. Debug le piège CNPG specific. Image PG-PostGIS-Timescale UID 26. Restore les hypertables depuis le dump NAS. (~2-3h)
  • Phase 7 — SeaweedFS K8s. Helm chart officiel, bucket gwc-tiles, ré-activer GWC S3 et JDBCStore, scale GeoServer à 2-3 replicas avec sticky session. (~3-4h)
  • Phase 8 — Observability. kube-prometheus-stack + Grafana dashboards (vessel rate, queue depth, decoder throughput, GS p95 latency) + Loki logs + AlertManager → Telegram. (~4-6h)
  • Phase 9 — Scaleway Kapsule. Provisioner le cluster cloud, sync ArgoCD app-of-apps cross-cluster, DNS public Cloudflare, cutover. (~1-2 weekend)

Le repo gitops public est github.com/Sylad/developpeur-gitops. Toute la documentation détaillée (K8S-MIGRATION-ROADMAP.md) vit dans le repo maritime-atlas. Les sites publics répondent en local depuis les URLs *.dev.local ; en production future, ils auront le même chemin réseau via Cloudflare DNS + cert-manager Let's Encrypt.

Jour 2 · La nuit n'était pas finie

Au réveil le 14 mai, la stack tournait sur le cluster Big-Blue (mon laptop). Mais Big-Blue n'est pas allumé 24/7, et je cherche un emploi : impossible d'envoyer un lien LinkedIn qui meurt quand je ferme l'ordi. Le 14 mai a donc été consacré à rendre la stack publiquement et durablement live. Spoiler : 8 heures de plus, et 4 problèmes que je n'avais pas anticipés.

1. Mini-Blue rejoint la danse

Big-Blue = laptop, donc fragile. Mini-Blue = GEEKOM IT13 i9-13900HK 32 GB qui tourne déjà 24/7 chez moi pour un sidecar grib-parser. J'y bootstrappe k3s en parallèle avec le même script, je restore les dumps PG sur cette seconde instance via kubectl cp + psql -f (méthode binary-safe — un | psql avait corrompu un COPY plus tôt dans la nuit). En 1h30, Mini-Blue a le même état que Big-Blue.

Le tunnel Cloudflare configuré sur un seul token accepte plusieurs cloudflared en parallèle : je déploie un cloudflared par cluster, et CF voit 2 connecteurs × 4 quic = 8 origins HA qui se load-balancent automatiquement. Quand j'éteins Big-Blue, le traffic glisse sur Mini-Blue sans intervention. Zero DNS failover, zero keepalived.

2. Le bug undici / Alpine musl libc

L'orchestrateur de données déclenche des fetch() HTTPS toutes les N minutes vers USGS earthquakes, hubeau débits FR, METAR, FIRMS MODIS, Météo-France. Toutes tombaient en "TypeError: fetch failed" après 5 secondes. CoreDNS résolvait pourtant correctement les hôtes externes, nc 1.1.1.1:443 passait, https.request() de Node renvoyait du 200… mais fetch() timeoutait, y compris sur IP directe.

Diagnostic isolé : l'image node:22-alpine (Alpine 3.23 + musl libc) déclenche un bug dans undici (l'implémentation HTTP de fetch()) qui ne se reproduit pas sur Debian. Aucun --dns-result-order=ipv4first ni --no-network-family-autoselection ne contourne. Fix radical et durable : passer la base à node:22-bookworm-slim. L'image gagne 600 MB mais retrouve un HTTPS sortant fiable — 9/9 erreurs ingestion disparaissent en 2 minutes.

J'ai figé ça en mémoire Claude (undici_alpine_musl_external_fetch_bug.md) avec un test de reproduction et la liste des services à migrer en Debian. Découverte qui m'aurait coûté 2 jours sur un prochain projet si je ne l'avais pas écrite ce soir-là.

3. La cérémonie deploy devient insoutenable

Pour pousser une image custom sur les 2 clusters, je répétais : docker builddocker save -o /tmp/X.tarscp -O /tmp/X.tar mini-bluessh mini-blue 'sudo k3s ctr -n k8s.io images import …'sudo k3s ctr -n k8s.io images import côté Big-Blue → bump tag values.yaml → commit → force ArgoCD refresh. ~10 minutes par image, × 14 services à migrer = inacceptable.

À mi-après-midi je bascule sur GitHub Container Registry (GHCR, gratuit avec mon compte) : token write:packages, docker login ghcr.io, tag/push chacune des 14 images, secret regcred créé une fois sur chaque cluster, et tous les Deployment templates référencent imagePullSecrets: - name: regcred.

Le workflow devient 3 lignes :

docker push ghcr.io/sylad/maritime-X:vY
./scripts/upgrade-app.sh maritime vY
# → 2 clusters synchronisés en ~60s, zéro touche humaine

Cette pivot a changé toute la suite : les vagues 1 + 2 de migration NAS se sont enchaînées sans friction.

4. Parité totale NAS docker-compose en deux vagues

Le chart maritime de la nuit ne couvrait que 5 services (api, frontend, geoserver, ais-decoder, alerts-engine, track-builder). Le NAS en avait 20. L'après-midi est consacré aux deux vagues de complétion :

  • Vague 1 (45 min) : ais-ingester (image rebuild Debian, Secret AISSTREAM_API_KEY), lightning-fetcher (websocket continu), buoy-fetcher (EMODnet WFS, Python 3.12).
  • Vague 2 (1h30) : grib-parser (sidecar Python GDAL/cfgrib), weather-fetcher (GFS), weather-fetcher-arpege (Météo-France ARPEGE), weather-fetcher-arome (AROME), sst-fetcher (NOAA OISST). Ces 5 services partagent un volume /coverage/ via hostPath (quick fix car k3s local-path est RWO uniquement). Lecture par GeoServer dans le même pod, écriture par les fetchers.

Résultat à 14:50 du 14 mai : 17 pods Running sur chaque cluster, 8 layers WMS GeoServer alimentés, flux AIS live à 11 000 messages/min, foudre en streaming WSS, bouées EMODnet seedées, vessels positions qui rentrent dans TimescaleDB.

5. Les vrais gotchas du quotidien post-migration

Au-delà des 7 pièges de la nuit, le jour 2 a ajouté :

  • ALTER OWNER en boucle DO à rejouer après CHAQUE service applicatif qui crée une table/view au boot (postgres reste owner par défaut → permission denied côté user applicatif). Couvrir tables ET sequences ET views — j'ai oublié les views au premier round.
  • GeoServer datastore.xml hardcodé au hostname Swarm (postgres) au lieu du service K8s (pg-data-rw.maritime.svc.cluster.local). Fix : sed in-place dans le PVC + restart pod GS.
  • Liveness probe cloudflared par défaut pointe :2000/ready mais cloudflared bind metrics sur un port aléatoire → 12 restarts en 26 min de crashloop silencieux. Fix : arg explicite --metrics 0.0.0.0:2000.
  • kubectl wait --for=ready match l'ANCIEN pod pendant un rollout restart (label sélecteur identique) — préférer kubectl rollout status.

Bilan à H+14 du déclencheur initial

  • 2 clusters k3s (Big-Blue + Mini-Blue) en HA via Cloudflare tunnel
  • 17 services orchestrés en ArgoCD GitOps (vs 20 sur NAS, 3 obsolètes)
  • 14 images Docker publiées sur GHCR, pulled automatiquement
  • ~150 GeoTIFF / jour écrits par les fetchers météo dans /coverage/
  • Auth Google OAuth opérationnelle, NAS docker-compose dépréciable
  • Coût mensuel : €0. Économie vs Scaleway Kapsule équivalent : ~€47/mo

Ce que ces 14 heures m'ont surtout appris : une fois la base solide, les déploiements suivants sont gratuits. La friction des premières migrations est ce qui force à construire un workflow propre. Le jour 3 (cette nuit, 24h plus tard) je pousse une image en 2 commandes et tout est en ligne.

Méta · 0 ligne de code humaine

Ce déploiement a été conduit en pair-programming avec Claude Code (Opus 4.7) agissant comme dev-devops de mon équipe d'une personne. Je n'ai écrit aucune ligne de YAML, aucun Dockerfile, aucun manifest ArgoCD ; mais c'est moi qui ai pris chaque décision d'architecture, validé chaque trade-off, commit chaque diff après revue. C'est aussi moi qui ai apporté les connaissances métier critiques (le 302 GeoServer /geoserver/web/, la licence MinIO 2024 qui forçait SeaweedFS, le pattern dual-DB).

Le portfolio entier est sous cette signature explicite : badges Claude / ChatGPT dans chaque README, Co-Authored-By systématique dans les commits, blogs assumés. En 2026, le différenciant n'est plus "je sais coder", c'est "je sais piloter une équipe IA senior". Cette nuit en est un cas pratique.