Comment j’exploite mes serveurs (2022)
(blog.wesleyac.com)- Les services personnels sont exploités directement sur une VM DigitalOcean à 5 $/mois avec Debian 10 ; pour des services de petite taille, installer les logiciels serveur sur une VM reste une approche suffisamment simple et raisonnable
- Les applications serveur sont construites sous forme de binaires statiques Rust, et l’artefact de déploiement est maintenu comme un fichier unique incluant HTML, CSS, configuration et secrets
- L’exécution est gérée par systemd, tandis que HTTPS est confié à un reverse proxy nginx et à Let’s Encrypt/certbot, afin que l’application n’ait pas à gérer TLS directement
- Les services qui ont besoin de données utilisent un fichier SQLite unique, avec à la fois des sauvegardes quotidiennes Tarsnap et des sauvegardes en streaming via Litestream vers DigitalOcean Spaces
- Lorsque plusieurs services cohabitent sur une même VM, ils sont isolés par utilisateur Unix propre à chaque service ; si une sécurité plus forte est nécessaire, une VM séparée, systemd-nspawn ou firejail sont utilisés
Configuration de base des serveurs et du déploiement
- Plusieurs services comme thoughts.page, hanabi.site, cgmserver et phonebridge fonctionnent avec une configuration proche de celle-ci
- Les applications s’exécutent sur une VM DigitalOcean ; l’offre est le palier à 5 $/mois et le système d’exploitation est Debian 10
- Certains services partagent la même VM, d’autres sont séparés sur une autre VM
- Les logiciels serveur sont écrits en Rust
- Ils sont liés statiquement
- HTML, CSS, configuration, secrets, etc. sont compilés dans le binaire
- rust-musl-builder et rust-embed sont utilisés
- Avec cette approche, le déploiement se résume à copier un fichier unique sur le serveur
- Une approche similaire semble possible avec Go, C++, etc.
- Pour les langages où le déploiement sous forme de binaire unique n’est pas facile, une alternative consiste à utiliser un conteneur Docker comme artefact de build
Exécution, proxy et conservation des données
- Le démarrage des services est géré par systemd
- Le binaire est lancé avec le serveur au démarrage
- La plupart des fichiers unit systemd sont de simples fichiers de 9 lignes
- systemd lui-même est complexe, mais pour démarrer un serveur au boot, on rencontre peu cette complexité
- Le déploiement est pris en charge par un simple script de déploiement
- Il copie le binaire sur le serveur et redémarre le serveur
- Le rollback est possible, et une version valide est toujours exécutée même si la connexion est interrompue pendant le déploiement
- Les programmes qui ont besoin d’une base de données utilisent SQLite
- Tout l’état de l’application tient dans un fichier unique
- Une sauvegarde est créée chaque jour avec la commande SQLite
.backupet stockée dans Tarsnap - Le script de sauvegarde est exécuté via cron
- Avec Litestream, une copie de la base de données est diffusée en streaming toutes les quelques secondes vers le stockage DigitalOcean Spaces, et des snapshots sont créés toutes les 6 heures
- Tous les serveurs s’exécutent derrière un reverse proxy nginx
- nginx gère la terminaison TLS, l’application n’a donc pas à se préoccuper de HTTPS
- Les certificats HTTPS sont obtenus via Let’s Encrypt et certbot, puis renouvelés automatiquement
- Un exemple de configuration nginx pour hanabi.site se trouve dans un gist séparé
- Les fichiers statiques peuvent être servis par nginx, et il suffit de les copier sur le serveur avec
scpoursync
Maintenance et isolation des services
- Cette configuration vise un chemin d’exploitation simple et robuste
- En dehors de l’application elle-même, les logiciels présents sur le chemin de service sont des composants éprouvés utilisés depuis des décennies
- Tant que la facture DigitalOcean est payée, l’exploitation du site ne demande pratiquement aucune maintenance fondamentale
- Les seuls problèmes détectés par la surveillance ont été des incidents temporaires du réseau DigitalOcean
- Le système d’exploitation et les mises à jour de sécurité sont parfois nécessaires
- Les versions Debian bénéficient de 5 ans de support, il faudra donc passer à Debian 11 dans environ deux ans et demi
- Si un événement comme Heartbleed se reproduit, un correctif sera nécessaire
- Ce genre d’événement reste plutôt rare
- Le coût d’une VM à 5 $/mois par service peut être pesant, mais plusieurs services peuvent tourner sur la même VM
- L’isolation est assurée en créant un compte utilisateur Unix distinct pour chaque service
- Cette méthode d’isolation existe depuis les débuts d’Unix, elle semble donc robuste
- Si une isolation plus forte est nécessaire, on peut utiliser systemd-nspawn ou firejail
- Si la sécurité est vraiment critique, on paie 5 $/mois supplémentaires et on exécute le service sur une VM séparée
- La procédure de configuration d’un nouveau projet est courte
- Créer un nouvel utilisateur
- Ajouter un hôte virtuel nginx et émettre un certificat HTTPS avec certbot
- Ajouter une unit systemd
- Commiter le script de déploiement dans le dépôt et l’exécuter
- Au début, il y a beaucoup à apprendre, mais cette infrastructure évolue bien plus lentement que l’infrastructure cloud
- Le format de configuration de nginx est resté essentiellement presque identique au cours des 10 dernières années
- Le dernier grand changement dans l’administration système Debian remonte à près de 10 ans, avec la transition vers systemd
- On est moins exposé aux situations où un fournisseur cloud abandonne un service ou en modifie silencieusement le comportement
- La seule dépendance est le fournisseur de VPS, et comme les serveurs sont un produit générique, il est possible de migrer vers un autre fournisseur
1 commentaires
Commentaires Hacker News
Si vous exécutez chaque service sous un utilisateur Unix distinct pour l’isolation, la fonctionnalité DynamicUser de systemd peut faire gagner du temps
Elle attribue un UID et crée les répertoires de logs/état avec les bonnes permissions
https://0pointer.net/blog/dynamic-users-with-systemd.html
Il suffit de placer dans
/etc/sysusers.d/un petit fichier texte contenant des informations comme le nom d’utilisateur et le répertoire personnel, puis d’exécuter la commande ou le servicesysusersLes fonctions cloud déclenchées par HTTP sont ce que je préfère en ce moment
Si on évite soigneusement les pièges propres à chaque fournisseur, cela réduit fortement la complexité, et c’est la seule abstraction cloud native qui donne vraiment l’impression d’être une « forme finale »
J’en ai déployé plus de 2 000 fois sur une seule app, et je n’ai jamais eu une seule fois à contacter le support parce que l’environnement d’exécution était cassé, ni à taper des commandes console obscures
Les performances sont excellentes, même avec le plan App Service isolé d’Azure, et mon rejet idéologique de la facturation à la requête a désormais disparu
Posséder aussi l’immobilier où se trouvent ses propres serveurs peut certes coûter moins cher, mais pour recréer soi-même des propriétés comme la conformité ou l’auditabilité qu’apportent de simples fonctions HTTP, il faudrait en pratique monter une énorme entreprise et embaucher une armée de gens
L’argument de la dépendance au fournisseur me parle aussi beaucoup moins qu’avant. Une interface qui reçoit une requête HTTP et renvoie une réponse HTTP est naturelle, et les différences entre fournisseurs se limitent surtout à du contexte annexe comme la signature de la méthode de déclenchement ou des claims OIDC/SAML ; il faudrait presque faire exprès pour construire quelque chose qu’on ne pourrait pas refactorer vers un autre fournisseur en moins d’une semaine
Pour un blog personnel, je comprends qu’il soit plus amusant d’acheter une VM Hetzner et de la peaufiner comme un artisan. Dans un secteur totalement dépourvu de régulation, on peut aussi défendre plus fermement une approche qui examine de très près les marges et la complexité plutôt qu’une externalisation totale des serveurs
Je me demande comment il pense que tout le monde faisait avant le cloud
Aujourd’hui, je supervise plusieurs équipes dans une grande entreprise industrielle et je fais évoluer/déployer des apps d’innovation sur Azure, et le coût du cloud est absurde par rapport au nombre d’utilisateurs
Avant, nous faisions tourner des apps avec 10 fois plus d’utilisateurs pour 1/100 du coût, avec une complexité plus faible
Comme cette facture est refacturée à d’autres départements qui ont fait ce choix douteux, cela ne me touche pas personnellement, mais j’ai du mal à comprendre cette foi aveugle dans le cloud
Pour nous, l’exploitation sur notre propre matériel est la meilleure solution, et comme on n’a même pas le droit de prendre des photos dans les bureaux, la probabilité de confier quoi que ce soit à un fournisseur cloud est de 0 %
Mais pour qu’une app devienne utile, il faut aussi d’autres éléments comme une base de données ; est-ce que les coûts n’augmentent pas à ce moment-là ?
Je ne sais pas s’il s’agit d’un gestionnaire de requêtes d’un serveur HTTP, d’un service cloud en mode function-as-a-service, ou simplement d’un vieux RPC
Si un script kiddie commence à envoyer 10 requêtes par minute, au bout de combien de temps le coût de la fonction dépasse-t-il celui d’un VPS ?
J’utilise une configuration similaire pour mon site personnel et mes sites de projet
J’utilise une VM Linode à 5 dollars par mois, Debian GNU/Linux, et j’écris le logiciel en Common Lisp
Pour mon site personnel ou mon blog, je génère un site statique avec un programme Common Lisp ; pour les services en ligne ou les web apps, je crée des programmes Common Lisp qui traitent les requêtes HTTP et renvoient les réponses avec Hunchentoot
J’utilise des fichiers d’unité systemd pour que les sites/services démarrent automatiquement au lancement ou au redémarrage de la VM, et ils font généralement entre 10 et 15 lignes
J’ai codifié la configuration initiale de la VM dans un script shell : https://github.com/susam/dotfiles/blob/main/linode.sh
La configuration par projet et par service est gérée avec un Makefile pour chacun : https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
Je n’utilise pas de conteneurs. Ces sites tournent depuis des années, bien avant que les conteneurs deviennent à la mode, et jusqu’ici les scripts d’initialisation et les Makefile ont suffi
J’utilise aussi Nginx. Il sert les fichiers statiques et, lorsqu’il y a un service backend, joue le rôle de proxy inverse ; en plus de la terminaison TLS, il apporte aussi des avantages comme la limitation du débit des requêtes ou la configuration de listes blanches d’en-têtes HTTP pour protéger le backend
Mon petit playbook personnel contient quelques commandes comme
curl LINK -o linode.sh && sh linode.sh,git clone LINK && cd PROJECT && sudo make setup httpsLes cibles
makes’occupent d’installer les outils nécessaires au fonctionnement du site, comme Nginx, certbot ou sbcl, de configurer Nginx, de configurer les certificats, etc., et une fois la commande terminée, le site web est immédiatement en ligneEn regardant le Makefile, on dirait que vous utilisez SBCL, et je me demande si l’utilisation mémoire a déjà posé problème
J’ai un VPS avec 512 Mo de RAM, et une instance SBCL consomme environ 100 Mo, donc je ne peux lancer que quelques services en même temps
J’ai même envisagé de migrer les services avec le moins de trafic vers CLISP, mais il lui manque l’une des fonctionnalités que j’utilise, à savoir les alias locaux de paquetages
Après avoir continué à peaufiner la configuration pendant plusieurs années, j’en suis arrivé aujourd’hui à faire tourner absolument tout dans des conteneurs Docker
Comme orchestrateur, j’utilise docker-compose plutôt que systemd, et Caddy plutôt que nginx pour le proxy
Comme dans le billet original, j’écris un script de déploiement pour chaque projet à exécuter, donc au final c’est assez similaire dans l’ensemble
Parmi les nombreux avantages de Docker, il y a le fait de pouvoir aussi exécuter des logiciels tiers avec la même configuration
J’utilise cette configuration depuis des années et elle est excellente : comme dans le billet d’origine, elle est solide tout en restant suffisamment flexible pour être modifiée comme on veut si nécessaire
Mon seul vrai point de douleur aujourd’hui, c’est le déploiement progressif. À mesure que le logiciel grossit, les quelques secondes d’indisponibilité à chaque déploiement deviennent un problème, et je n’ai pas encore de solution simple, mais docker swarm est peut-être la bonne direction
Pour réduire l’indisponibilité, on peut essayer
health_uri /health,lb_try_duration 30sPar exemple, avec une configuration comme
reverse_proxy api:8089 { health_uri /health lb_try_duration 30s }, Caddy mettra les requêtes en tampon pendant le déploiement de la nouvelle version et lui laissera 30 secondes pour démarrerIdéalement, Caddy ne devrait commencer à utiliser la nouvelle version qu’une fois celle-ci en bonne santé, puis arrêter l’ancien conteneur
J’ai regardé https://github.com/Wowu/docker-rollout et https://github.com/lucaslorentz/caddy-docker-proxy, mais je n’ai pas encore réussi à en faire une priorité
Ce n’est pas parfait, mais au lieu de recevoir une erreur de connexion, le navigateur reste simplement en chargement pendant quelques secondes
https://mrsk.dev/ utilise la même technique
La production a commencé avec compose, puis nous l’avons améliorée plus tard avec un pipeline de déploiement continu qui met automatiquement la stack à niveau
À mesure que l’entreprise et la base d’utilisateurs ont grandi, des problèmes d’indisponibilité sont apparus lors des redémarrages ou des déploiements, et il fallait préparer un nouvel environnement de déploiement chaque fois qu’on voulait faire tourner une application supplémentaire
J’appréhendais le jour où il faudrait utiliser Kubernetes, car j’en avais déjà vu la complexité de près et je n’avais aucune envie de passer la majeure partie de mes journées à cajoler un cluster
Nous sommes donc passés au mode Swarm, et le voyage a parfois été Dr Jekyll, parfois Mr Hyde
Il y a des bugs que personne ne cherche à corriger, des pans de la spécification Docker qui ne sont pas implémentés sans que cela soit signalé, des choix d’implémentation à s’arracher les cheveux, et l’impression que les employés de Docker Inc se parlent peu entre eux ou n’arrivent pas à rester concentrés jusqu’au bout
Malgré cela, il y a aussi beaucoup de beauté. Les stacks compose continuent de fonctionner telles quelles, tout en permettant de monter en charge là où c’est nécessaire, et avec une bonne configuration, les déploiements sans interruption, les mises à niveau, l’équilibrage de charge et les rollbacks fonctionnent bien
Raft reste fiable pour maintenir le cluster, comme ailleurs, et avec un peu d’effort, on peut construire une plateforme self-service flexible, sûre et automatiquement distribuée, pour une fraction du budget de maintenance de K8s
En revanche, il faut être prêt à bien construire ses scripts de déploiement. J’ai passé pas mal de temps à créer des outils Python qui convertissent des fichiers compose conformes à la spécification Docker en spécification Swarm, renouvellent et nettoient les secrets, et étendent les variables d’environnement
Selon le fournisseur de VPS, il faut aussi impérativement régler correctement le MTU réseau. J’ai probablement perdu quelques années de vie à cause de ça
du moins tant que les problèmes de montée en charge ne deviennent pas réels avec l’augmentation du nombre de clients ou d’utilisateurs
docker saveetdocker importJe me demande si vous exploitez votre propre registry
Sur les serveurs physiques, je mets la base de données PostgreSQL et l’application dans des Pods Podman, le tout sur des ports localhost au-dessus de 5000, et Caddy sert de proxy inverse sur le port 443
Un timer systemd exporte toutes les bases de données chaque jour à 16 h 55 dans un répertoire unique
Ensuite, DejaDup [1] sauvegarde automatiquement
$HOMEchaque jour à 17 h sur un disque dur externe, en excluant les fichiers de cache mais en incluant les exports des bases de donnéesL’OS est Debian avec GNOME Core [2], et les règles firewalld n’autorisent que les ports 80, 443 et un port SSH personnalisé
SSH utilise des clés et l’authentification par mot de passe est désactivée
C’est la méthode la plus ennuyeuse, mais elle fonctionne, tout simplement
1 - https://flathub.org/apps/org.gnome.DejaDup
2 - https://packages.debian.org/bookworm/gnome-core
Ces temps-ci, j’utilise Dokku sur un seul serveur
C’est simple à administrer, les développeurs peuvent déployer avec un simple
git pushdans le style Heroku, et il y a beaucoup de plugins, donc ajouter une base de données ou configurer le HTTPS prend au plus 10 secondesgit pushest vraiment pratiqueAjouter une appli, ajouter une base de données, gérer les variables d’environnement et les domaines : tout est très intuitif
J’aime les configurations simples, donc cette approche a l’air agréable à utiliser
Elle sera probablement suffisante pour 99 % des services
J’utiliserais sans doute Ansible pour la configuration des serveurs et les scripts de déploiement
Dans ce cas, les serveurs peuvent être documentés et les scripts de déploiement peuvent aussi devenir plus simples
Cela dit, je n’éviterais pas de me connecter directement au serveur pour déboguer ou vérifier des choses
Mes scripts n’ont presque pas changé depuis 7 ans, mais Ansible est lent et a tendance à nécessiter la maintenance continue de plusieurs fichiers
Pour ce qui est du débogage, on peut aussi exposer les logs de l’application via nginx sur un endpoint protégé par mot de passe
On semble parfois oublier que CI/CD et une gestion efficace des serveurs étaient déjà des pratiques courantes avant même le cloud
À propos de l’idée d’écrire le logiciel serveur en Rust, de le lier statiquement, puis de compiler directement dans le binaire le HTML, le CSS, la configuration, les secrets, etc. : je fais récemment cela en Go, et j’adore à quel point cela simplifie l’écriture et le déploiement de logiciels qui dépendent de fichiers statiques
Utiliser des variables d’environnement ou un fichier de configuration pour les secrets est plus proche de la pratique standard, et même si cela ajoute une étape avant l’exécution, cela évite de partager un binaire en oubliant qu’il contient des secrets
Il est assez facile d’extraire un binaire et d’y chercher des chaînes de caractères
En revanche, embarquer les assets statiques du front-end et une configuration par défaut présente un gros avantage
Avec Rust, surtout pour faire de la compilation croisée de Mac vers Linux, c’est relativement pénible, alors qu’avec Go c’est trivial et intégré à l’outil
goCela facilite vraiment l’élimination des frictions au moment de terminer et déployer un side project
J’ai juste une question. Dans l’article, il est écrit : « Les certificats Let’s Encrypt sont obtenus avec certbot et leur renouvellement automatique est également géré », mais j’utilise une configuration multi-région avec deux serveurs et du geo-DNS, donc certbot ne fonctionne que sur le serveur américain et je dois copier manuellement le certificat vers le serveur européen
Y a-t-il un moyen de résoudre ça ?
La description de ClickHouse Playground est ici : https://ghe.clickhouse.tech/
Je sers aussi l’application web depuis le même service compilé statiquement qui fournit l’API backend
En lançant
npm builddans la CI puis en embarquant le résultat, il devient très facile de faire des tests en local ou de démarrer une instance de démo