Ce que nous avons trouvé utile dans NixOS
Cette note technique d’implémentation porte sur l’architecture d’infrastructure et de récupération de Curling IO v3. Elle s’adresse aux ingénieurs logiciels et aux responsables de l’exploitation, et aborde NixOS, le provisionnement de serveurs physiques, les secrets, le déploiement, les sauvegardes et la validation de récupération plus en détail que nos articles habituels sur le produit.
Curling IO fonctionnait auparavant sur Debian configuré avec Ansible. Cette installation marchait. Elle installait les paquets, configurait Caddy et le pare-feu, créait les services systemd et préparait nos emplacements de déploiement bleu-vert.
Le problème venait du fait qu’un serveur fonctionnel et modifiable nous permettait de conserver une description incomplète. Ansible pouvait décrire le serveur, mais d’anciens fichiers restaient sous /etc. Des outils de compilation avaient été installés autrement. Les choix du fournisseur sur les disques et le RAID existaient en dehors du playbook. Un provisionnement pouvait réussir parce qu’une exécution antérieure avait déjà laissé le bon élément en place.
NixOS tolère beaucoup moins ces lacunes. Cette rigueur est parfois agaçante, mais constitue aussi son principal avantage. Elle nous oblige à décider qui est responsable d’un fichier, d’un service, d’un secret, d’un paquet ou d’un élément nécessaire à la récupération.
Le passage d’Ansible à NixOS nous a montré combien de validations et de nettoyages manquaient à l’ancienne approche. Il a exposé la différence entre un serveur qui continue de fonctionner et un serveur qu’on peut recréer à partir d’éléments déclarés.
Voici ce que nous avons trouvé utile en reconstruisant un serveur physique OVH à partir de disques vides, en restaurant son état et en rendant le processus assez sûr pour être répété.
La frontière que nous voulions
Nous voulions répondre à une question simple : si le serveur disparaissait, pourrions-nous reconstruire Curling IO rapidement, sans état non documenté de l’ancien hôte et pratiquement sans perte de données clients?
Une récupération réussie devait :
- Recréer les disques en miroir et la disposition de démarrage EFI.
- Activer la configuration exacte de l’hôte.
- Rétablir sa capacité à déchiffrer ses secrets.
- Restaurer un état Caddy avec des certificats valides.
- Compiler et déployer la plateforme, le site public et l’application Operations depuis une seule révision Git figée.
- Restaurer et valider SQLite depuis Litestream.
- Vérifier le stockage des fichiers téléversés publics et privés.
- Créer et vérifier un instantané chiffré de la base de données dans une deuxième région.
- Réussir les vérifications locales, publiques, de stockage, de surveillance et du fournisseur.
Cela dépasse le provisionnement du système d’exploitation. NixOS est responsable de l’hôte, mais les versions applicatives et l’état des données clients ont leurs propres parcours.
| Élément | Responsable |
|---|---|
| Disposition des disques et installation initiale | OVH BYOLinux et notre hook de démarrage de l’image de récupération |
| Paquets et services du système d’exploitation | Configuration NixOS |
| Configuration générée de l’hôte | Chemins versionnés du magasin Nix |
| Activation des secrets de l’hôte | Parcours de récupération SOPS |
| Compilations de la plateforme, du site public et d’Operations | Parcours de déploiement indépendants |
| Installation et activation des versions | Commandes de déploiement indépendantes côté serveur |
| Migrations de base de données | Parcours de déploiement de la plateforme |
| Surveillance de l’hôte et des applications | Application Operations déployée indépendamment |
| Base clients et fichiers téléversés | Parcours Litestream et Object Storage |
Cette séparation tient bien. Nous pouvons changer l’hôte sans déployer une application, déployer la plateforme sans déployer le site public et restaurer une base sans réinstaller le serveur.
Les versions applicatives restent distinctes
NixOS est responsable de l’hôte, pas de la compilation des applications. Nous compilons la plateforme, le site public et Operations avant le déploiement, les emballons en versions vérifiées, puis les téléversons par SSH. La production n’a besoin ni de code source, ni d’identifiants de dépôt, ni de chaînes de compilation ou de caches de compilation.
L’empreinte logicielle de production est donc plus petite : moins de paquets et d’outils exécutables, aucun dépôt de sources ou identifiant de compilation et moins d’état de compilation modifiable. Cela réduit la surface d’attaque et le fardeau des correctifs, sans remplacer le cloisonnement des services ou les mises à jour rapides.
Les changements d’hôte et les versions applicatives restent indépendants. La récupération a aussi une frontière simple : compiler depuis une révision Git figée, vérifier chaque paquet avant et après son téléversement, puis l’activer par le même parcours de déploiement qu’en fonctionnement normal.
Nix a deux parcours distincts de déploiement de l’hôte
Le provisionnement habituel et la récupération sur un hôte de remplacement fonctionnent volontairement différemment.
Pour un changement normal de configuration, notre script bin/provision production envoie les sources d’infrastructure exactes et validées dans Git au serveur en fonctionnement. Celui-ci utilise Nix pour construire une nouvelle génération système immuable, la valider, y passer et garder la génération précédente pour un retour arrière.
La récupération sur un hôte vierge ne peut pas dépendre de ce parcours. Une image de récupération censée être complète ne devrait pas démarrer pour ensuite télécharger ou construire la majorité du système réel. Notre image BYOLinux contient donc :
- Un système d’amorçage minimal sans secrets, capable de démarrer et d’accepter SSH.
- La fermeture NixOS de production complète, c’est-à-dire tous ses éléments et dépendances, comme contenu inactif du magasin.
Après le premier démarrage, la commande de récupération active le système de production déjà intégré et ses secrets requis. Si l’image ne contient pas la fermeture nécessaire, la récupération s’arrête. Elle ne masque pas l’image incomplète en se rabattant sur une construction à distance.
L’image de récupération est donc un véritable artefact plutôt qu’un mince installateur. Les commits touchant seulement les applications peuvent la réutiliser, car sa dérivation dépend uniquement des sources d’infrastructure. Le code applicatif, la documentation et les fichiers de tâches n’imposent pas de reconstruire une image de plusieurs gigaoctets.
Le serveur physique garde le fournisseur dans la conception
NixOS n’élimine pas OVH du parcours de récupération.
Notre premier installateur créait des métadonnées RAID logiciel valides avec un noyau Linux plus récent. Le noyau NixOS installé et le noyau de secours d’OVH refusaient tous deux de les assembler. Mettre seulement NixOS à jour aurait fait fonctionner le système installé, mais aurait laissé l’environnement de secours du fournisseur incapable de lire les disques.
Nous avons changé la répartition des responsabilités. OVH BYOLinux crée maintenant les systèmes de fichiers ext4 en miroir selon la disposition prise en charge par le fournisseur. Notre image fournit le système de fichiers NixOS et un hook de démarrage qui étiquette les systèmes de fichiers et installe GRUB EFI au chemin de média amovible sur les deux disques.
Le hook BYOLinux du fournisseur fonctionne dans un chroot restreint. Lors des premiers essais, il n’avait ni l’interpréteur de commandes ni le PATH attendus. La réussite de la construction d’image ne disait rien de la capacité d’OVH à exécuter le hook. Il a fallu :
- Utiliser un chemin absolu du magasin Nix pour Bash.
- Intégrer le hook directement plutôt que d’appeler un deuxième chemin du magasin absent de l’image.
- Utiliser les commandes internes de l’interpréteur jusqu’à la disponibilité du
PATHdu système intégré. - Tester chaque chemin du magasin référencé contre l’image elle-même.
- Rejouer le hook dans un chroot semblable à celui du fournisseur.
C’était décevant. QCOW2 offre un format d’image standard, mais pas un contrat standard pour en faire une installation physique démarrable. Nous pensions que la frontière du fournisseur serait plus prévisible. OVH exécutait plutôt un hook qui lui était propre dans un chroot minimal, dont il fallait découvrir l’interpréteur, le PATH et les chemins du magasin par des tests. Le format était portable, pas le contrat d’installation.
La surveillance du fournisseur comptait aussi. Lors d’une première répétition, OVH a détecté l’interruption prévue et ouvert une intervention technique. Celle-ci a bloqué une demande d’API de changement de mode de démarrage. La vérification préalable désactive maintenant l’intervention automatique et le confirme avant toute réinstallation. Elle rétablit ensuite la politique choisie lorsque l’hôte récupéré a réussi ses vérifications.
Résoudre cette frontière avec le fournisseur a pris la majeure partie du temps d’ingénierie consacré au provisionnement de récupération. La réinstallation réussie a pris moins de huit minutes, mais y arriver a exigé des constructions d’images répétées, des hooks en échec, des démarrages de secours et des répétitions destructives.
La configuration générée exige un seul responsable
Caddy a vite exposé le problème de la configuration modifiable.
L’ancien serveur avait des fragments générés copiés dans des répertoires modifiables. Après un changement de disposition, des fichiers périmés restaient et étaient encore chargés. La configuration était correcte dans le dépôt, mais erronée sur le serveur, qui contenait à la fois les anciennes et les nouvelles interprétations.
La configuration NixOS possède maintenant les fichiers Caddy générés sous forme de liens vers des chemins exacts du magasin Nix. Les chemins obsolètes sont supprimés explicitement, et la configuration assemblée est validée avant l’activation.
Cette leçon dépasse NixOS. Un déploiement Ansible peut imposer la même règle, mais doit le faire délibérément. Copier le dernier fichier souhaité ne suffit pas si un ancien fichier peut encore influer sur l’exécution.
L’état de récupération doit exister avant l’effacement
Le serveur en panne n’est pas une source de récupération.
Avant de l’effacer, la vérification préalable contrôle :
- La révision d’infrastructure et l’image de récupération choisies.
- Les éléments requis pour récupérer les secrets.
- L’état actuel du RAID.
- Une sauvegarde déchiffrable de l’état Caddy avec des certificats valides.
- La réplique Litestream hors site et l’instantané chiffré dans un deuxième emplacement hors site.
- Les frontières de stockage des fichiers téléversés publics et privés.
- Les points d’accès publics et l’état OVH.
L’état Caddy est devenu un élément explicite après notre constat que l’émission des certificats ne devrait pas se trouver sur le chemin critique de récupération. NixOS vérifie les changements d’état Caddy toutes les cinq minutes et stocke des copies chiffrées hors site. Des emplacements de rétention hebdomadaire offrent un filet de sécurité.
Pendant la récupération, Caddy reste arrêté le temps de préparer la plateforme, le site, la base, les fichiers téléversés et l’instantané chiffré. Il ne démarre que lorsque le serveur peut présenter un système complet avec les certificats restaurés. Le renouvellement normal en arrière-plan reprend ensuite.
SQLite suit un parcours distinct. Litestream réplique continuellement hors site. Une tâche quotidienne restaure et valide cette réplique avant de créer un instantané complet, chiffré côté client, dans un deuxième emplacement hors site. La restauration empêche qu’un ancien instantané de sinistre se réplique vers la réplique principale pendant son test.
La récupération part d’une révision figée
Une longue récupération ne peut pas suivre en toute sécurité une branche qui change.
La commande complète crée une copie de travail temporaire au commit choisi. La publication d’image, l’installation par le fournisseur, l’activation de l’hôte, les trois compilations applicatives, la restauration de l’état et la validation finale partent de cette arborescence figée. De nouveaux commits dans la copie normale ne peuvent pas modifier une récupération en cours.
Le parcours possède aussi des points de reprise durables :
- Vérifier l’image et les éléments de récupération.
- Demander à OVH d’installer l’image.
- Activer la fermeture NixOS intégrée.
- Construire localement les paquets applicatifs vérifiés.
- Déployer la plateforme et le site public.
- Restaurer et valider SQLite, puis vérifier le stockage des fichiers téléversés.
- Démarrer Caddy.
- Déployer Operations.
- Exécuter les vérifications finales d’acceptation.
Si le déploiement applicatif échoue après le démarrage de l’hôte, nous reprenons au déploiement applicatif. Nous n’effaçons pas les disques une nouvelle fois. Le basculement du DNS public est une autre opération distincte et confirmée, après les vérifications directes de l’hôte de remplacement.
Les mises à jour utilisent la même vérification de santé
NixOS cherche les mises à jour stables chaque nuit et garde la génération précédente pour un retour arrière. Une nouvelle génération n’est acceptée qu’après avoir réussi les mêmes vérifications de santé en lecture seule que la surveillance Operations.
Le provisionnement, les mises à jour automatiques, la récupération et la surveillance partagent donc une définition d’un serveur sain. Si les vérifications échouent, l’hôte retourne à la génération précédente.
Les leçons au-delà de NixOS
La même rigueur permet d’aller assez loin avec Debian et Ansible :
- Provisionner depuis une révision exacte.
- Définir explicitement les responsabilités et le nettoyage.
- Fixer les versions des outils de récupération.
- Protéger les commandes destructives.
- Tester automatiquement tout le parcours sur un hôte de remplacement.
Rien de cela n’exige NixOS. La différence est qu’Ansible permet de sauter ces étapes et de continuer. NixOS rend les désaccords avec le système déclaré beaucoup plus difficiles à ignorer. Nous aimons ces garde-fous techniques chez Curling IO.
Décision
Nous gardons NixOS.
Il ne compile pas nos applications, ne restaure pas les données clients, n’élimine pas le fournisseur et ne remplace pas les scripts de déploiement. Il donne à ces systèmes un hôte mieux défini et une frontière d’activation plus sûre.
Le plus utile n’est pas la reproductibilité comme propriété abstraite. C’est l’obligation de rendre explicites les hypothèses cachées :
- Quel système est responsable de ce fichier?
- Quelle clé peut récupérer ce secret?
- Quel artefact identifie cet hôte?
- Qu’est-ce qui survit à la perte du serveur?
- Qu’est-ce qui prouve qu’une nouvelle génération est saine?
- Où la récupération peut-elle reprendre après un échec?
Ces questions ont amélioré tout le système de déploiement, y compris les parties sans Nix.
Cet article est le neuvième de la série sur les fondations de Curling IO. Précédent : un panneau d’administration en direct sans écrire de JavaScript.