7 points par GN⁺ 2023-07-17 | 1 commentaires | Partager sur WhatsApp
  • 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 .backup et 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 scp ou rsync

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

 
GN⁺ 2023-07-17
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

    • Si l’UID doit être persistant, il est aussi très simple de créer un nouvel utilisateur système avec systemd-sysusers
      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 service sysusers
    • Utiliser un compte de service distinct pour chaque service/app est une approche assez standard, et il vaut mieux aussi séparer les serveurs
  • Les 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

    • J’ai levé les yeux au ciel en lisant le passage sur le fait qu’« il faudrait créer une entreprise d’un milliard de dollars et embaucher 1 000 personnes de plus pour l’implémenter soi-même »
      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
    • Je travaille dans un secteur extrêmement réglementé, mais je ne comprends pas la dernière phrase
      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 %
    • Les fonctions ont une place correcte pour héberger une API simple
      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 vois pas exactement ce qu’on entend ici par fonction cloud déclenchée par HTTP
      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
    • Ce qui compte, c’est ce que fait réellement la fonction
      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 https
    Les cibles make s’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 ligne

    • Je me demande quelle implémentation de Common Lisp vous utilisez
      En 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

    • J’utilise aussi Caddy dans une configuration similaire
      Pour réduire l’indisponibilité, on peut essayer health_uri /health, lb_try_duration 30s
      Par 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émarrer
      Idé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é
    • Si un équilibreur de charge comme Caddy se trouve devant les pods, on peut le configurer pour retenir les requêtes pendant que le nouveau pod démarre : https://twitter.com/bradleyjkemp/status/1486756361845329927
      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
    • En construisant dès le départ la stack logicielle de la startup où je travaille, nous avons directement empaqueté les applications avec Docker
      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
    • Si vous avez déjà un fichier compose, Swarm est la voie logique, mais personnellement j’ai jugé que ça n’en valait pas la peine
      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
    • J’ai monté une configuration similaire, mais je n’aime pas l’idée de pousser les images par SSH avec docker save et docker import
      Je 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 $HOME chaque jour à 17 h sur un disque dur externe, en excluant les fichiers de cache mais en incluant les exports des bases de données
    L’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

    • Je me demande pourquoi faire tourner GNOME sur un serveur
    • Je me demande s’il y a une raison d’utiliser des timers systemd plutôt que des tâches cron
  • Ces temps-ci, j’utilise Dokku sur un seul serveur
    C’est simple à administrer, les développeurs peuvent déployer avec un simple git push dans 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 secondes

    • J’utilise la même approche, et le fait de pouvoir déployer avec git push est vraiment pratique
      Ajouter 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

    • J’utilise Ansible, mais je me demande à nouveau si ça en vaut la peine
      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

    • Inclure des secrets dans un binaire compilé reste malgré tout discutable
      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
    • Je n’aime pas particulièrement Go, mais je continue à l’utiliser pour cette raison
      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 go
      Cela facilite vraiment l’élimination des frictions au moment de terminer et déployer un side project
    • https://play.clickhouse.com/play?user=play fonctionne aussi de cette manière
      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/
    • Moi aussi, j’utilise cette approche
      Je sers aussi l’application web depuis le même service compilé statiquement qui fournit l’API backend
      En lançant npm build dans 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