Aller au contenu

Appliquer les migrations de la base

Faire évoluer le schéma de la passerelle sans détruire les clés, et la migration dont on ne revient pas.

Cette page décrit comment amener la base d’une passerelle déjà déployée au schéma courant, et la migration qui ne se défait pas.

La liste des migrations qui vont s’exécuter ne se lit dans aucun document. Elle est calculée par le script, sur les fichiers présents dans migrations/ et sur ce que la base a déjà reçu. Une copie tenue à la main de cette liste vieillit ; un administrateur qui la suit peut croire poser un renommage de contrainte quand la chaîne contient une migration irréversible.

Fenêtre de terminal
export DATABASE_URL="postgresql://<utilisateur>:<mot-de-passe>@<hôte>:<port>/<base>"
cd gateway-db
./migrate.sh --status

Chaque ligne [ ] est une migration qui sera exécutée au prochain appel sans option. Lisez-les toutes avant de lancer quoi que ce soit.

Une migration s’applique sur une sauvegarde qu’on sait restaurer

Section intitulée « Une migration s’applique sur une sauvegarde qu’on sait restaurer »

Prendre une sauvegarde ne suffit pas. Ce qui protège est d’avoir déjà restauré une sauvegarde au moins une fois, à froid, sur cette installation. Tant que l’aller-retour n’a pas été fait, vous avez des fichiers dont personne ne sait s’ils remontent.

La procédure (les deux scripts, l’inventaire à comparer avant et après, l’essai de restauration sur une base de côté) est dans Sauvegarder et restaurer la base de la passerelle. Faites-la avant de revenir ici.

Deux points qu’elle détaille et qui pèsent sur la suite :

  • la sauvegarde ne porte pas les secrets qui vivent hors de la base : ni les clés de vos fournisseurs, dont la base ne connaît que le nom de la variable d’environnement, ni ADMIN_API_KEY. Une base restaurée ne fait pas une installation qui repart ;
  • le fichier est un secret : avant la migration 015, il contient les clés de tous vos porteurs en clair.

Les instantanés gérés par votre hébergeur restent utiles, mais ils ne remplacent pas cet essai : ni leur cadence, ni leur rétention, ni leur contenu exact ne sont vérifiables depuis ici.

Elles sont listées, dans l’ordre où elles s’appliquent, par la référence des migrations. Cette page est produite depuis les fichiers eux-mêmes : elle ne peut ni oublier une migration qui existe, ni en décrire une qui n’existe pas. Ce guide n’en tient pas de copie.

Le script trie les noms de fichiers et n’exige aucune contiguïté : un trou dans la numérotation n’est pas une migration perdue.

C’est la seule migration de la chaîne dont on ne revient pas.

Avant elle, users.api_key contient la clé de chaque client en clair. La migration calcule le condensat SHA-256 de chaque clé, en dérive le fragment affichable (sk-lemniscate-…a3f9), puis supprime la colonne.

Après le DROP COLUMN, aucune requête, aucun écran, aucun accès à la base ne retrouve une clé. La conversion est sans rupture pour les porteurs : personne n’a à changer de clé. Ce qui disparaît est votre capacité à relire une clé, donc à réparer une remise ratée autrement qu’en faisant tourner la clé.

Une sauvegarde antérieure devient alors le seul endroit où ces clés existent en clair. Traitez-la comme un secret : chiffrez-la, sortez-la de la machine, détruisez-la une fois la migration confirmée. La garder « au cas où » rouvre la fuite que la migration ferme.

Le tout tient dans une transaction unique, avec l’inscription au registre : la migration passe entièrement ou pas du tout.

L’ordre compte. Le schéma et le code ne sont pas compatibles en avant : le code d’avant lit api_key, qui n’existe plus, et le code neuf ne fonctionne pas sur l’ancien schéma. Il y a donc une fenêtre pendant laquelle l’authentification est en panne, de la durée du redéploiement. Prévoyez-la.

  1. Sauvegarder, et traiter le fichier comme un secret.

    Fenêtre de terminal
    ./sauvegarder.sh --sortie /var/sauvegardes/avant-migration-$(date +%Y%m%d-%H%M%S).sql.gz

    Gardez l’inventaire que le script affiche : c’est ce que vous comparerez si vous devez revenir en arrière. Le détail est dans Sauvegarder et restaurer la base de la passerelle.

  2. Relever ce qui va s’exécuter.

    Fenêtre de terminal
    ./migrate.sh --status
  3. Compter les comptes avant, pour avoir un nombre à retrouver après.

    Fenêtre de terminal
    psql "$DATABASE_URL" -c 'SELECT count(*) FROM users'
  4. Appliquer.

    Fenêtre de terminal
    ./migrate.sh

    Chaque migration s’exécute dans une transaction unique avec son inscription au registre schema_migrations : une migration qui échoue ne laisse ni schéma à moitié converti, ni registre faux.

  5. Vérifier, si 015 ou 021 faisait partie du lot appliqué. Les trois nombres doivent être égaux entre eux et égaux à celui de l’étape 3.

    Fenêtre de terminal
    psql "$DATABASE_URL" -c "SELECT count(*) AS cles, count(*) FILTER (WHERE key_hash ~ '^[0-9a-f]{64}$') AS hachees, count(*) FILTER (WHERE key_hint <> '') AS avec_fragment FROM access_keys"

    Aucune colonne de clé ne doit subsister sur les comptes ; zéro ligne est attendue :

    Fenêtre de terminal
    psql "$DATABASE_URL" -c "SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name LIKE '%key%'"
  6. Mettre les rôles de service en service, si 023 faisait partie du lot appliqué.

    Cette migration crée deux rôles PostgreSQL, lemniscate_gateway et lemniscate_console, avec, pour chacun, les seuls droits dont son service a besoin. Elle les crée sans mot de passe et sans le droit de se connecter (NOLOGIN) : un secret écrit dans le produit serait un secret publié chez tous ceux qui l’installent. Ouvrez-les avec vos propres secrets :

    Fenêtre de terminal
    psql "$DATABASE_URL" -c "ALTER ROLE lemniscate_gateway WITH LOGIN PASSWORD 'votre-secret'"
    psql "$DATABASE_URL" -c "ALTER ROLE lemniscate_console WITH LOGIN PASSWORD 'un-autre'"

    Puis remplacez, dans la configuration de chaque service, l’identifiant du compte propriétaire par celui de son rôle. La passerelle prend lemniscate_gateway, la console lemniscate_console.

  7. Redéployer la passerelle et la console, dans un ordre indifférent entre elles, mais après la migration.

  8. Contrôler. Authentifier une clé connue contre la passerelle, et ouvrir la liste des comptes dans la console : chaque ligne doit porter son fragment.

migrate.sh tient un registre (schema_migrations) qu’il crée au premier appel. Une base dont le schéma a été monté autrement que par le script n’a pas ce registre. Le script, ne trouvant rien d’inscrit, voudrait tout rejouer depuis 000, ce qui détruirait des données vivantes.

Le jalonnement sert à ce cas, une seule fois :

Fenêtre de terminal
./migrate.sh --baseline 006 # inscrit 000 à 006 sans les exécuter
./migrate.sh # applique 007 et les suivantes

Ensuite, ./migrate.sh sans option applique les migrations suivantes.

La valeur 006 n’est pas générique. C’est celle d’une base dont l’histoire est connue : 001 à 006 y ont été passées à la main, sans registre. --baseline inscrit sans exécuter : un jalon posé trop loin marque comme appliquées des migrations qui ne l’ont pas été, et ces migrations ne s’exécutent plus. Sur 015, cela laisse des clés en clair dans une base que le reste du système croit convertie.

Ne posez un jalon que sur une base dont vous savez, par ailleurs, jusqu’où le schéma est monté.

Le second cas est celui d’une base créée par docker-compose, qui a reçu init.sql. Ce fichier décrit l’état cible complet du schéma, et un test d’intégration compare colonne par colonne les deux constructions. La chaîne ne s’y applique pas telle quelle : 000 y recrée des tables qui existent déjà, et échoue. Le jalon à poser est celui de la dernière migration du dépôt ; la référence des migrations le donne, et le tutoriel Découvrir la passerelle le prescrit aussi.

Si vous étendez le schéma vous-même, trois gestes, dans cet ordre :

  1. créer migrations/0NN-description-courte.sql ; le préfixe à trois chiffres fixe l’ordre d’exécution ;
  2. répercuter le même effet dans init.sql, qui décrit l’état cible complet ;
  3. lancer la suite d’intégration : le test de schéma échoue si les deux descriptions ont divergé.

La migration 010 ne se retouche pas : son en-tête l’interdit, parce qu’elle s’exécute sur un schéma qui a encore users.api_key. Une clé publiée après 015 se purge dans une migration neuve, sur le condensat.