blog.vfg.lol

Hero v2 en production : données migrées, jeton tourné, retour possible

La prod de Hero servait un build de juin, avec un jeton d'écriture exposé dans sa page. On a figé la version en service en release de retour, sorti la base sans l'arrêter, retiré le FTP au profit du VPS, déployé la v2 en migrant les données en place, puis tourné le jeton — en mesurant chaque étape.

TL;DR — hero.vfg.lol servait une version du 14 juin, et sa page publiait le jeton d'écriture en clair. Avant de toucher quoi que ce soit, on a figé la version en service dans une release de retour exacte (le tag existant était deux commits trop ancien). Puis on a découvert un piège invisible : la base de données n'était pas persistée — le volume Docker était monté sur le mauvais dossier, si bien qu'un simple redéploiement aurait effacé les données. On a donc exporté la base vivante sans arrêter le service, retiré la chaîne FTP (elle échouait depuis des semaines), déployé la v2, puis réimplanté la base — que la nouvelle version migre en place. Résultat : niveau 7, 997 XP, neuf quêtes, tous servis après coup. Et un retour en une commande.

Le point de départ, mesuré

Deux constats, chacun daté et rejouable :

  • la page publique env.js exposait window.__API_TOKEN__ — le jeton qui ouvre une session d'écriture ;
  • l'API affichait environ 88 jours d'uptime : elle n'avait pas été redémarrée depuis des mois.

Deux chaînes de déploiement coexistaient pour la même adresse : un FTP OVH (en échec d'authentification à chaque poussée) et une chaîne Docker vers le VPS (build d'image, puis démarrage à distance), bridée par des secrets manquants. La prod n'avançait donc plus.

Figer la version en service — pour pouvoir revenir

La règle avant toute mutation : savoir exactement à quoi revenir. On a comparé les versions d'assets servies par la prod (base v17 / lcd v18 / panels v34) à l'historique du dépôt : elles correspondaient exactement à un commit du 16 juin. Le tag de release existant, lui, était deux commits plus ancien — un piège, parce qu'un retour dessus n'aurait pas restauré la vraie version.

On a donc créé un tag de retour fidèle sur le commit réellement servi, hors du motif qui déclenche les automatisations (v*) — pour qu'un simple gel ne relance aucun build. Une release publiée l'accompagne, avec l'empreinte de l'image. Revenir = une commande.

Le piège de données caché

C'est en cherchant à exporter que le vrai danger est apparu. La base vivait dans un dossier éphémère du conteneur, alors que le volume persistant était monté sur un dossier voisin. Conséquence : la base n'était jamais sauvegardée, et le script de sauvegarde de la flotte archivait… un volume vide. Redéployer, c'était perdre niveau, XP, quêtes et journal.

Le fichier était pourtant bien vivant — modifié quelques minutes plus tôt — et volumineux : 1,5 Mo, seize tables, dont plus de 7 000 événements de pont et 1 000 lignes de journal.

Sortir la base, sans l'arrêter

On a copié le fichier hors du conteneur sans interrompre le service, puis vérifié son intégrité (empreinte stable, en-tête SQLite valide) et relu son contenu — niveau 7, 997 XP, neuf quêtes, dix-huit jauges — pour être sûr d'emporter des données réelles et pas une coquille vide. Un second export a suivi juste avant la bascule, par principe.

Une seule chaîne : le VPS, pas le FTP

Le FTP a été neutralisé (déclenchement manuel, sans effet). La source de vérité est la chaîne Docker : build et publication de l'image, puis démarrage sur le VPS via le script canonique (deploy.sh), qui sauvegarde, tire l'image, relance les conteneurs et peut tester.

On a ensuite fusionné les deux branches en attente (un audit, puis la v2), créé un tag de version v2.0.0, et laissé la CI publier l'image correspondante.

Déployer, migrer en place

Le compose a été corrigé pour monter le volume au bon dossier de données. La base exportée a été déposée dans ce volume, avec la bonne propriété (le conteneur tourne en utilisateur non racine — un volume neuf appartient à root, ce qui provoque un refus d'accès au premier démarrage). Puis deploy.sh a relancé les deux conteneurs sur l'image v2.

Au démarrage, la nouvelle version migre la base existante sur place : ses migrations sont idempotentes (une colonne déjà présente est ignorée), donc les données survivent et se complètent. C'est le sens du « réimport » ici : on ne repart pas de zéro, on fait évoluer la base en service.

Tourner le jeton, et recâbler le pont

Le jeton ayant été public, il a été régénéré. Un jeton d'appartenance circule entre Hero et la porte VFGate : changer l'un sans l'autre casse le pont — les deux ont donc été mis à jour et les services concernés recréés (un jeton figé à la création du conteneur). Vérifié après coup : le nouveau jeton est accepté, l'ancien est refusé, et le pont répond à nouveau.

En chemin, un second bug latent a été corrigé : un chemin d'environnement erroné dans le compose empêchait toute recréation du service de la porte — ce qui aurait bloqué la rotation.

Ce qu'on garde

Sur les six contrôles de sortie, tout est vert :

ContrôleRésultat
page env.jsaucun jeton
écriture sans sessionrefusée
fiche publique200 — niveau 7, 997 XP, 9 quêtes
page d'adminredirigée vers la connexion
santé APIOK, uptime faible (donc bien redémarrée)
front200, assets de la v2

Et le mot de passe propriétaire (la phrase de connexion) est posé, distinct du jeton. Le retour reste possible à tout moment : une commande pour remettre l'image de juin, et les sauvegardes de configuration et d'environnement sont conservées à côté.

La leçon n'est pas « on a déployé une v2 ». C'est qu'un déploiement sans avoir mesuré ce qui tourne et où vivent les données aurait pu effacer quatre mois de vie du personnage, sans un seul message d'erreur. Figer, exporter, mesurer — avant d'écrire quoi que ce soit.

source markdown

Concepts de cet article

VPSgardeVFGateVFG Hero

Partage le plus de concepts avec