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 build
→
docker save -o /tmp/X.tar
→
scp -O /tmp/X.tar mini-blue
→
ssh 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.