Journal Douze facteurs dans ta tronche

24
5
nov.
2022

Aujourd'hui, encore un journal qui dénonce grave.

Je voudrais m'insurger contre un nouveau genre de culte du cargo, le Twelve Factor App. Ce sont des principes d'architecture logicielle qui seraient adaptés à l'écriture de micro-services et qui promettent performance, qualité, et retour de l'être aimé.

Figurez-vous que j'ai au turbin quelques collègues qui ne jurent que par les 12 facteurs, et qui, en exégètes, font passer ces principes au dessus de tout, à toutes les sauces, et surtout sans réfléchir. Il ont l'impression qu'il leur suffit de déclamer que ce n'est pas 12 factors, et ils s'attendent à ce que je m'agenouille et demande pardon pour mes péchés et que j'aille immédiatement modifier le code scélérat.

Alors, je dis pas, j'aime bien les aphorismes à l'emporte pièce, et je suis le premier à lancer un KISS ou un YAGNI voire un petit DRY des familles. Mais, moins que des principes, ce sont des idées, des philosophies, qui nous guident sans nous contraindre. Ces idées sont justifiées et confirmées par l'expérience, mais l'expérience nous apprend aussi quand les laisser de côté.

Avec les 12 facteurs, ce qui m'interpelle tout d'abord, c'est généralement l'absence de justifications: ces principes sont énoncés, un poil développés, et c'est tout, demerden sie sich, comme on dit outre-Rhin. Alors, certains, c'est difficile de pas être d'accord, le principe 2 sur les dépendances qui doivent être explicites, par exemple, ça se tient.

Mais le 1, qui dit qu'il faut une base de code par application ? Non mais il fume quoi?

Et le 11, qui dit que les logs doivent être des événements ?

Bah laissez moi vous raconter, alors.

Il y a de cela 6 mois, mon collègue revoit mon code de logs, et ça lui plait pas : j'avais mis en place la possibilité de logger vers la sortie standard, vers un fichier, ou vers les deux. "Nan mais tu comprends, c'est pas 12 factors, ça va pas, faut enlever les logs vers les fichiers". Nonobstant le principe pourtant bien établi que qui peut le plus peut le moins, il me court sur le haricot et refuse de fusionner jusqu'à ce qu'on coupe la poire en deux : on garde le code de log vers les fichiers, mais on ne l'expose pas via l'application.

Le temps passe. Mon collègue intègre l'application comme un service systemd. Mais tous les logs dans journalctl, c'est pas très pratique, alors plutôt que de démarrer le service directement, systemd démarre un script bash qui démarre l’application en redirigeant la sortie standard vers un fichier.

Plusieurs semaines après, alors que je me plains que les fichiers générés par l'application ne sont pas finalisés correctement, il se rend compte que systemd envie un sigterm vers le script bash mais pas ses enfants, et le process ne reçoit pas le sigterm au bon moment. Parmi les différentes manières de corriger le problème, il se décide en maugréant à ressusciter le log vers les fichiers, permettant à SystemD de démarrer directement l’application. Et bien sûr, dans le message de commit, il s'est excusé de violer les 12 facteurs (mais mec, je m'en tamponne, de tes 12 facteurs !), et a expliqué que ce n'était que partie remise (puisque tout ça va se déplacer vers Kubernetes).

Alors, on va s'intéresser à un autre principe bien connu, hein ?

"Une taille unique ne convient pas à tous"

  • # Moui… mais…

    Posté par  . Évalué à 8.

    Je suis plutôt d’accord avec la vision « le dogme c’est mal ».

    Mais c’est quoi qui te gêne avec l’idée du « 1 repo pour 1 app » ?

    Et tu ne penses pas que pour les logs, pour une exécution dans un cloud type kubernetes, le point 11 soit également valide ? Dans le principe, on doit généralement considérer qu’on n’a aucun stockage à disposition puisque en cas du destruction du node, tout peut et va disparaître.

    • [^] # Re: Moui… mais…

      Posté par  (site web personnel) . Évalué à 6. Dernière modification le 05 novembre 2022 à 23:14.

      Merci de ta réponse !

      Alors, 1 repo pour 1 app, c'est un truc qui me semble super sur le papier, mais je ne vois pas comment le faire fonctionner efficacement passé une certaine complexité. Les systèmes avec lesquels je travaille ont typiquement des dizaines d'applications. Une moitié sont des applications de type "service", et l'autre moitié de type "outil console" pour contrôler ou requêter les services. Chaque application est typiquement très petite, et dépend de 3 ou 4 grosses bibliothèques avec beaucoup de fonctionnalités communes. Est-ce donc qu'il me faut un dépôt par bibliothèque, et 25 dépôts supplémentaires pour chaque application ?

      En fait, c'est là que je ne comprends plus. Par exemple, je change une API dans ma bibliothèque, faut-il que je déploie ma bibliothèque (tests, revue de code, fusion…), puis que j'aille dans chacun des 25 dépôts pour vérifier si ça compile, faire les changements, et faire 25 déploiements (tests, revue de code, fusion…) ? Et rebelotte si je me suis vautré au début ? Je ne comprends absolument pas en quoi séparer les dépôts aide à quoi que ce soit.

      Mais j'aimerais savoir si il existe un meilleur modèle, parce que sur le principe, un peu de modularité dans mon monorepo, j'aime bien, avec principalement l'idée de réduire le temps de compilation / test.

      Ensuite, sur les logs, en effet, dans un cloud Kubernetes, je suis d'accord : c'est logique de sortir les logs sur la sortie standard, et de laisser Kubernetes balancer tout ça vers un Elastic Search ou équivalent. Mais… Elastic Search, que c'est lent ! C'est pratique pour trouver des lignes de log corrélées entre plusieurs applications, mais pour explorer un simple fichier, j'ai du mal à trouver quelque chose d'aussi efficace que tail / grep / sed. Je me demande d'ailleurs si il serait possible d'avoir un volume NFS rien que pour les logs, et d'avoir l'application qui monte ce volume et qui logge dessus, afin de pouvoir taper dedans plus facilement ?

      Bref, je cherche encore, et je suis preneur de conseils.

      • [^] # Re: Moui… mais…

        Posté par  (site web personnel) . Évalué à 10.

        Je ne comprends absolument pas en quoi séparer les dépôts aide à quoi que ce soit.

        Tu ne sépare pas les dépôts, mais les codebases.

        Que tu sois en monorepo ou pas, chaque libraire a son propre dossier, avec son propre package.json/pom.xml/pyproject.toml/…

        Que tu sois en monorepo ou pas, chaque application liste la dépendance dans son package.json/pom.xml/pyproject.toml/…

        L'idée est de passer par le gestionnaire de paquet du langage pour gérer la dépendance.

        Je cite, de 12factor:

        If there are multiple codebases, it’s not an app – it’s a distributed system. Each component in a distributed system is an app, and each can individually comply with twelve-factor.

        Donc un monorepo pour gérer un système distribuer, cela s'entend parfaitement. Et c'est ce que beaucoup font.


        c'est logique de sortir les logs sur la sortie standard, et de laisser Kubernetes balancer tout ça vers un Elastic Search ou équivalent.

        SystemD est capable de rediriger stdout/stderr dans un fichier, ainsi ton application print sur stdout/stderr, et tu laisse l'environnement d'exécution gérer le reste (rotation de logs, compression de log, redirection vers un service centralisé ou non, stockage dans un NFS, etc…)

        Je me demande d'ailleurs si il serait possible d'avoir un volume NFS rien que pour les logs, et d'avoir l'application qui monte ce volume et qui logge dessus

        C'est pas à ton application de monter ce volume. C'est à SystemD, ou a Kubernetes, ou tout autre environnement d'exécution.

        https://link-society.com - https://kubirds.com

        • [^] # Re: Moui… mais…

          Posté par  (site web personnel) . Évalué à 3.

          Bon, admettons: j'ai un monorepo, qui contient mes 3 libs et mes 25 binaires, je fais un changement de lib, revue de code, fusion, ça part dans le package manager, puis je mets à jour les dépendances de mes binaires en un coup (puisque monorepo), revue de code, fusion. J'ai donc réduit de 26 fusions à 2, ce qui est vachement mieux !

          À voir à l'usage, donc. J'ai vraiment cette habitude de compiler tout mon système d'un coup, et de monter et descendre dans mes dépendances dans tous les sens pour ajuster mon code. Encore du mal à vraiment voir ce que ça m'apporte de séparer plus.


          Effectivement, dans le problème initial que je présente, la solution eut été que SystemD s'en occupe, mais apparemment la version que nous utilisons ne le permet pas. Dans le cas de pods, je comprends l'idée de dire que c'est à kubernetes de balancer les logs là où il faut, et sur le principe, j'aime bien, parce que partir à la chasse du bon fichier de log, c'est plutôt lourd. Mais peut-être que Elk / ElasticSearch ne sont pas encore à la hauteur.

          En début de semaine, j'ai du sortir l'équivalent d'une liste d'utilisateurs connectés, pour un besoin ponctuel. Bah, j'avais un beau fichier de log, grep | sed | sort -u, directement vers un fichier que je peux envoyer à qui le voulait. Avec ElasticSearch, c'est possible?

          Bref - Y'a de l'idée, mais j'ai pas l'impression que ça soit encore ça…

          • [^] # Re: Moui… mais…

            Posté par  (site web personnel) . Évalué à 5.

            binaires, je fais un changement de lib, revue de code, fusion, ça part dans le package manager, puis je mets à jour les dépendances de mes binaires en un coup (puisque monorepo)

            Il n'est pas inconcevable de lister dans le package.json (ou équivalent) file:../some_other_dep

            Le problème de faire comme ça et que tu créé un couplage fort, un bug dans la dépendance impacte directement tout ceux qui en dépendent de cette manière. Et le rollback devient plus compliqué.

            Encore du mal à vraiment voir ce que ça m'apporte de séparer plus.

            De la robustesse et de la reproductibilité. Tu n'es pas obligé de mettre à jour tes 25 services d'un coup. Chacun peut dépendre d'une version différente.

            Disons que ton service A est impacté par un bug dans ta dépendance. Mais le service B n'utilise pas la fonction buguée de cette même dépendance. Tu peux donc en parfaite isolation corriger le bug dans la dépendance, mettre a jour le service A, et redéployer uniquement le service A.

            Ou alors, Jean Michel qui est le mainteneur de la dépendance est en vacance. On peut rollback aisément, il suffit de corriger le package.json/pom.xml/pyproject.toml, et on n'impacte pas les autres services.


            Avec ElasticSearch, c'est possible?

            Avec Kibana oui. La stack ELK c'est :

            • ElasticSearch pour indexer
            • Logstash pour récupérer les logs et les transmettre dans un format unifié à ElasticSearch
            • Kibana pour aller grep dans le ElasticSearch

            Ici le point important c'est que Logstash va transformer tes logs:

            level=debug time="some iso datetime" Something happened!!!
            

            Sera transformé par logstash (suivant ta configuration de ce dernier) en:

            {
              "level": "debug",
              "time": "some iso datetime",
              "message": "Something happened!!!"
            }

            Si tu as un second service qui log cependant :

            [debug] [some iso datetime] Something happened!!!
            

            Le document qui sera indexé dans ElasticSearch sera le même. Ainsi, l'ops n'a plus à se préoccuper du format des logs lorsqu'il va composer son grep/sed/sort, il va simplement filtrer en fonction des champs du document indexé dans ElasticSearch.

            Pour ton exemple:

            [info] [some iso datetime] User alice is connected
            

            Pourrait être transformé en :

            {
              "source": "application-foobar",
              "level": "info",
              "time": "some iso datetime",
              "user": "alice",
              "event": "connected"
            }

            En gros, c'est comme si tu configurais le grep/sed/awk/whatever au niveau du logstash. Ainsi les données que ElasticSearch ingère sont déjà structurée, ce qui facilite le querying.

            C'est overkill pour des petits setups, mais quand tu commences a compter tes services par centaines, la centralisation c'est plutôt pas mal. L'ops n'a même pas à forcément connaitre l'application, il peut directement mettre en place du monitoring de log et automatiser la notification des équipes le tout de manière dynamique.

            Cette stack répond a des besoins très précis, de niche je dirais même, car tout le monde n'est pas Google.

            Il faut aussi se rendre compte que l'architecture microservice répond à un besoin organisationnel, et non technique. Quand tu as 20 équipes de 4 personnes qui doivent travailler sur des aspects différents d'un même projet, le monolithe (même modulaire) ralenti tout le monde. Dans une précédente mission, l'entreprise était en cours de réécriture d'un monolithe en Java (bien modularisé) vers les microservices. Les 10 équipes sont passés de "attendre 30min après chaque commit que la CI/CD du monolithe fasse son job" à "attendre 5min après chaque commit que la CI/CD de son service fasse son job".

            Et je parle pas du workflow de PR sur le monolithe avec des phrases du genre "on merge quelles PR aujourd'hui ? que je sache si je vais me prendre un café ou si je travaille".

            https://link-society.com - https://kubirds.com

          • [^] # Re: Moui… mais…

            Posté par  (Mastodon) . Évalué à 7.

            Je ne comprends pas pourquoi tu parles d'aller chercher dans les logs manuellement ou de configurer des services via systemd. Les 12 factors, on s'en sert généralement pour des applications déployées sous forme de containers / fonctions.

            Le problèmes de rapidité que tu rencontres avec ton instance d'elasticsearch n'ont rien à voir avec tout ça. D'une part parce que elasticsearch n'est pas un prérequis des 12 facteurs, que vos clusters sont peut-être mal dimenssionnés et d'autre part parce qu'il existe plein d'alternatives.

            Tu utilises des détails non pertinents pour étayer ton propos.

            • [^] # Re: Moui… mais…

              Posté par  (site web personnel) . Évalué à 4.

              Les 12 factors, on s'en sert généralement pour des applications déployées sous forme de containers / fonctions.

              Le principe des 12 factor c'est que tu sais pas comment va être déployé ton application.

              En suivant ces principes, tu peux déployer via systemd, via openrc, via sysvinit, via docker compose, via kubernetes, via nomad, via …

              https://link-society.com - https://kubirds.com

              • [^] # Re: Moui… mais…

                Posté par  (Mastodon) . Évalué à 3.

                Oui mais tu vas t'appuyer sur une infra qui est mise en place pour que tout ce que tu aies à faire, c'est du déclaratif. Et certainement pas aller te logger individuellement sur les machines pour aller fouiller dans les logs ou modifier des units systemd. D'ailleurs tu n'as même pas à savoir que c'est systemd ou un autre superviseur qui va démarrer ton appli.

    • [^] # Re: Moui… mais…

      Posté par  . Évalué à 2.

      Et tu ne penses pas que pour les logs, pour une exécution dans un cloud type kubernetes, le point 11 soit également valide ?

      La sortie standard n'est-elle pas ce fichier /dev/stdout ? Dans l'exemple de l'auteur, il a adopté la flexibilité en laissant le choix, et ça me semble judicieux.

    • [^] # Re: Moui… mais…

      Posté par  . Évalué à 8. Dernière modification le 07 novembre 2022 à 03:31.

      Sans aller dans le mono vs multiple repo, ce que dit le site en question c’est tout simplement une appli doit être 100% contenue dans un seul repo (en gros pas le droit de faire un checkout de 2 repos pour devoir builder une seule appli, donc pas de git submodules), et que si du code est partagé, alors ce code doit être construit sous forme de bibliothèque (je suppute pour offrir une gestion de version).

      C’est tourné de façon un peu alambiqué, mais c’est du bon sens.

      Ça n’exclue pas le mono repo, mais ça implique que chaque « sous repo » soit capable de pointer vers une version spécifique de sa dépendance. En gros, si tu veux faire du monorepo, il te faut un système similaire à ce que fait Google en interne. Et ca n’exclue pas le multi repo.

      Le 1 repo 1 app est un problème pour des systèmes distribués, typiquement le backend de n’importe quel site de taille raisonnable. Quand t’as plus de 100 services qui contribuent tous au backend, ça veut dire 100 repos. Ça devient plus dur de trouver le code, de faire des scans automatique etc. C’est des problèmes qui peuvent se résoudre, mais le mono repo les évite par design. Après, ça apporte d’autres problèmes, qui peuvent aussi se résoudre. Comme toujours c’est une question de compromis.

      Pour le point 11, c’est aussi du bon sens: tes logs sont juste un flux d’événements. Au bout du flux se trouvent des appenders. Ils peuvent écrire sur le disque s’ils veulent, sur stdout s’ils veulent, ou balancer tout ça à ta stack ELK. En gros ce que fait logback ou log4j de base. Au call site, tu te contente de dire log.info("Successfully refubulated the automatic magnetic flux"), sans te préoccuper de savoir ou ça peut bien aller (potentiellement, nulle part).

      Bref, ce que je vois surtout dans ce journal, c’est 2 personnes qui ont des problèmes de compréhension de l’anglais.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

  • # Explication

    Posté par  (site web personnel) . Évalué à 10.

    1 - One Codebase tracked in revision control, many deploys

    Je ne l'interprète pas comme "une application = un dépôt", mais plutôt comme "on déploie a partir de ce qui est dans le dépôt".

    Si tu as un mono-dépôt, contenant l'ensemble de tes services/applications, tu déploies à partir de ce dépôt. Si tu as un dépôt par application, tu déploies à partir de chacun d'eux. Si tu as une application découpée en plusieurs dépôt (plusieurs modules par exemple), tu as un dépôt parmi eux qui décrit ton déploiement, et tu déploies à partir de ce dernier.

    L'idée ici suggère simplement que le dépôt est la source de vérité.

    2 - Explicitly declare and isolate dependencies

    On ne veut pas qu'un service/application impacte une autre. Pour Python, cela veut dire des virtualenv pour chaque application, pour NodeJS c'est le node_modules, pour Go et Rust c'est un binaire statique. On oublie pas non plus de lock les dépendances pour un max de reproductibilité.

    3 - Store config in the environment

    Tu ne sais pas comment ton application va être déployée, via un unit systemd ? via un conteneur docker ? via Kubernetes ? via une plateforme Function-as-a-Service (AWS Lambda ou autre) ? via Heroku ?

    Les variables d'environnement sont le dénominateur commun de toutes ces plateformes, il est donc logique de les utiliser pour donner le plus de flexibilité à l'Ops qui se chargera de mettre ton code en prod.

    Note que j'ai déjà vu des applications simplement configurer via les variables d'environment les accès à un Vault (hashicorp) et allait ensuite chercher sa configuration dedans. C'est parfaitement viable, l'Ops sait que la conf est dans le Vault, et sais comment configurer (via l'env) les accès Vault de l'application.

    4 - Treat backing services as attached resources

    Il s'agit simplement de considérer chaque service (local ou remote) qu'utilise l'application (base de données, serveur mail, API, etc…) comme remplaçable. Il ne faut pas hard-coder ce genre de service dans le code mais le rendre configurable pour que chaque déploiement puisse adapter l'infrastructure selon ses besoins.

    Bref, on mets l'accent sur le déploiement

    5 - Strictly separate build and run stages

    C'est simplement une bonne pratique de CI/CD. Tu ne veux pas installer les outils de dev pour build ton application sur le serveur de prod. Tu veux aussi pouvoir rollback aisément.

    Cela veut donc dire que tu dois avoir un endroit ou tu stockes tes releases après chaque build (Nexus? Docker Registry? Dépôt PyPI/Maven/whatever privé? un simple FTP/NFS/whatever?). L'Ops ensuite déploiera une release, et il a l'historique pour rollback.

    Sans cette séparation, il faudrait faire quelques git revert sur la branche main pour pouvoir rollback, c'est source d'erreur, et en général les gens ne connaissent que git commit/git push, donc ça va pas passer non plus.

    6 - Execute the app as one or more stateless processes

    L'idée ici est encore une fois de simplifier la vie lors des déploiements. Si tu as besoin de persister des données, tu délègues ça a un "backing service". On ne sait pas ou est déployée ton application, donc ça se trouve le système de fichier ne va pas persister entre 2 exécutions du service/application.

    De plus, avoir un backing service (même si c'est toi l'auteur) pour la persistance, permet de le réutiliser/mutualiser/etc… pour l'ensemble des services.

    Ce backing service, cela peut être : un GlusterFS, un FTP, une base de données, un PersistantVolume dans Kubernetes, etc…

    7 - Export services via port binding

    En gros, on fait du "proxy pass" dans notre gateway. Cela simplifie drastiquement le déploiement, peu importe ou et comment on déploie.

    Que ce soit aller modifier un NginX/Apache pour ajouter le vhost, ajouter un Ingress dans notre Kubernetes, ou aller ajouter un CNAME sur notre DNS, ou une règle de redirection dans iptables/pf, cela revient au même.

    8 - Scale out via the process model

    Un service devrait être découpé en plusieurs processus qui font une seule chose (web, worker, …). Ainsi lors du déploiement, on peut configurer combien de replicas pour chaque workloads (je veux 4 process web pour gérer les requêtes HTTP, 16 process worker pour gérer les tâches de fond, etc…).

    Encore une fois, l'idée est de donner plus de flexibilité au déploiement.

    9 - Maximize robustness with fast startup and graceful shutdown

    Gère SIGTERM correctement. Et spécifie quand ton service/application est ready.

    Pour Kubernetes, on va configurer des healthcheck. Cela peut être une commande qui fait une requête HTTP au service et s'assure qu'elle reçoit un 200 OK. Ou alors vérifier si un fichier /var/run/myservice.ready existe, ou tout ce que tu peux imaginer.

    L'idée est de permettre à la plateforme ou on déploie de s'assurer que le service s'est bien lancé. Ensuite, si on a de l'autoscaling, il se peut que notre instance se voit décommissionnée, il faut donc libérer les ressources proprement pour éviter toute corruption (si un upload de fichier est en cours, on attend qu'il soit fini par exemple avant de quitter).

    10 - Keep development, staging and production as similar as possible

    Bah oui, on veut éviter le "ça marche sur ma machine pourtant". Si le développeur est capable de tester en condition de production, c'est pas mal de bug trouvés à la source.

    11 - Treat logs as event streams

    En gros, oui l'idée c'est d'envoyer tout sur stdout et stderr.

    SystemD permet de rediriger stdout/stderr dans un fichier et de gérer la rotation/compression de logs.
    Je peux configurer une stack ELK ou autre pour récupérer les logs depuis /var/log (rempli par SystemD), depuis Docker, depuis Kubernetes, etc…

    Mon application elle? Elle print sur stdout et s'en fou de tout ça.

    Si chaque application fait comme elle sent, c'est d'autant plus de complexité pour le déploiement, car autant de cas spécifique à gérer. Cela a aussi au passage l'effet de simplifier le code de l'application qui n'a pas besoin de se préoccuper de cela.

    12 - Run admin/management tasks as one-off processes

    Que cela soit fait par des tâches ansible, par un rundeck, par un jenkins, par un init container dans Kubernetes, etc.. On s'en fout. Ce n'est tout simplement pas à l'application lors de son lancement de faire ce genre de chose.

    Idéalement, ton application ne tourne pas en tant que root. Donc il se peut en plus qu'elle n'ait pas les permissions pour faire telle ou telle tâche d'admin.


    Ensuite, sur https://12factor.net chaque point est un lien vers un article plus détaillé, expliquant le point de vu.

    Je conclurai tout simplement pour dire que cette philosophie, que tu es libre de ne pas suivre et d'adapter a ton besoin, mets l'accent sur le déploiement.

    Après des gens qui voient ça comme une religion a ne surtout pas blasphémer, c'est parce que l'humain est un stupide singe overclocké. Ce n'est pas la faute de 12factor.

    https://link-society.com - https://kubirds.com

    • [^] # Re: Explication

      Posté par  (site web personnel) . Évalué à 5.

      Merci de cette explication détaillée. En effet, certains points sont parfaitement raisonnables - Le 9 par exemple, je ne vois personne qui disent que ça doit prendre des plombes à démarrer et à s'éteindre !

      En revanche, sur d'autres, j'ai encore du mal : le coup des variables d'environnement, je trouve ça très implicite, pas facile du tout à découvrir, et sujet aux erreurs (typos?). Je préfère de loin une application qui prend en ligne de commande quelques éléments de base, comme un chemin vers un ou plusieurs fichiers de config, un --verbose, et autres. Comme ça, un --help indique immédiatement tout ce que je dois passer à l'app et tout ce que je peux faire avec. Et à ma connaissance, ça marche avec tous les langages, et tous les systèmes de déploiement que je connaisse (configmap K8 par exemple).

      • [^] # Re: Explication

        Posté par  . Évalué à 6.

        Sauf erreur de ma part, le fichier de configuration passé directement à l'application ne va pas fonctionner dans un environnement docker sans orchestrateur car tu vas devoir soit monter un volume pour exposer le fichier de configuration à l'application, soit reconstruire ton image avec le fichier de configuration dedans.

        Alors que tu peux passer un fichier de conf via --env-file qui mettra tout dans l'environnement sans lier ton conteneur à l'hôte ou devoir reconstruire ton image pour chaque environnement.

        Cependant, je reconnais que le fichier est peut-être moins découvrable. Une idée de palliatif (mais c'est probablement pas parfait) : en utilisant asciidoc (que j'aime beaucoup), j'inclus dans le README les variables utilisées (car je suis bien obligé de les définir à un endroit dans mon code et que ce n'est pas une gros effort de "tagguer la région" pour que le README soit à jour, sachant qu'il faut bien, à un moment, lire un bout de doc pour déployer).

        • [^] # Re: Explication

          Posté par  . Évalué à 4.

          Alors que tu peux passer un fichier de conf via --env-file qui mettra tout dans l'environnement sans lier ton conteneur à l'hôte ou devoir reconstruire ton image pour chaque environnement.

          En quoi passer un fichier de l'hôte contenant des variables d'environnement est différent de mapper un fichier de l'hôte contenant ta config ?

          https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

          • [^] # Re: Explication

            Posté par  . Évalué à 2.

            Tu as raison. Peut-être que préférer passer par --env-file plutôt que de monter un volume est une préférence personnelle de ma part.

            Après, j'ai eu quelques prises de têtes sur les volumes (mapping UID/GID par exemple, mais c'était avec Podman, peut-être que c'est différent avec Docker). C'est peut-être ça qui m'a un peu échaudé et me fait préférer --env-file si j'ai le choix (et mon application supportait la conf par variable d'environnement, --env-file était donc "gratuit").

            Par contre, si le choix c'est de monter un volume ou de faire évoluer l'application pour qu'elle supporte la configuration par variable d'environnement, c'est sûr que le volume est plus simple/rapide.

      • [^] # Re: Explication

        Posté par  (site web personnel) . Évalué à 3.

        En revanche, sur d'autres, j'ai encore du mal : le coup des variables d'environnement, je trouve ça très implicite

        C'est le seul moyen de faire à la fois propre et sans SPOF (single point of failure). Tu ne va pas mettre de mot de passe dans un fichier, fichier qui serait forcément sous GIT. Il existe des DB de configuration (etcd de mémoire) mais si elle tombe, tout tombe.

        Les outils modernes ont des options pour le DEV qui peuvent être setter par environnement. C'est ce qui est utilisé dans docker/k8s/PaS…

        "La première sécurité est la liberté"

        • [^] # Re: Explication

          Posté par  . Évalué à 2.

          Il existe des DB de configuration (etcd de mémoire) mais si elle tombe, tout tombe.

          C'est ce qu'utilise kubernetes, hein ? Si tu utilise kubernetes ta configuration en variable d'environnement « parce qu'etcd ça crée un SPOF », elle est stockée dans un etcd. En soit il n'y a pas de secret si tu ne veut pas être présent pour entrer le mot de passe à chaque nouveau pod qui démarre il va falloir que ce soit stocké quelque part1.

          Etcd peut tout à fait être clusterisé et s'il tombe ce n'est pas tout tombe, mais ton déploiement est figé (grosso modo les control plan ne peuvent plus prendre de décision).

          Après c'est intéressant de s'intéresser aux spof, mais généralement je vois des cluster avec un seul control plan, l'etcd intégré au control plan, une seule ligne internet, un seul provider de cloud, un seul dépôt git,… et pas de test régulier de faire tomber des brique pour s'assurer de l'absence de spof.


          1. il existe des systèmes qui vont créer une utilisateur à la volée pour chaque nouvelle instance qui le demande. Au démarrage tu demande un accès en lecture à ta base et le vault va te créer un utilisateur avec uniquement les droits dont tu as besoin. Je ne l'ai jamais expérimenté par contre. 

          https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

          • [^] # Re: Explication

            Posté par  (site web personnel) . Évalué à 3.

            Je pensais surtout à la manière de faire une brique logiciel "cloud ready" et pourquoi les variables ont aussi été choisi par les opérateurs de PaS. Et ils ont pensé à tout leur SPOF.

            Il est tout à fait légitime de ne pas gérer tous les spof. En gérer quelques uns fait effectivement remonter le "SLA" final.

            "La première sécurité est la liberté"

    • [^] # Re: Explication

      Posté par  (site web personnel) . Évalué à 3.

      Mais le 1, qui dit qu'il faut une base de code par application ? Non mais il fume quoi?

      Le problème n'est pas le monorepo, mais le multirepo inconsistant. Si tu as 30 applications, qui ne fonctionnent ensemble qu'en version précise, c'est une application distribuée et surtout pas
      des microservices. Le monorepo a l'avantage de gérer l'évolution parallèle des applications : c'est souhaitable mais pas toujours possible de gérer plusieurs versions d'API, pour éviter de devoir tout migrer d'un coup (syndrome du "monolithe distribué", aka l'enfer sur terre)

      Et le 11, qui dit que les logs doivent être des événements ?

      L'avantage des événements au sens DDD ou équivalent, ce sont des éléments auto-porteurs qui ne dépendent pas du contexte et sont des petits messages. Ils contiennent une date et des identifiants, ainsi on peut faire des pipelines de traitement en oubliant l'origine de l’événement (ELK, …). Un événement n'a ainsi aucun sens d'être modifiable : l’immutabilité offre beaucoup d'avantage.

      "La première sécurité est la liberté"

    • [^] # Re: Explication

      Posté par  . Évalué à 3.

      Gère SIGTERM correctement. Et spécifie quand ton service/application est ready.

      Je me demande combien d'applications savent que la seule façon à peu près fiable de gérer les signaux UNIX, c'est le self-pipe?
      Et que même le self-pipe peut casser en cas d'utilisation de multi-thread…

      Pour le reste, les variables d'environnement pour la config, moi ça me paraît une très mauvaise idée: tu parles d'avoir plusieurs instances d'un daemon dans un environnement, non? Pour la mise en échelle… des paramètres de ligne de commande me paraissent nettement plus simple moi. Qu'ils soient remplis par un programme qui les lise depuis un fichier de conf ou autre chose, peu importe, mais c'est le seul moyen que je voie qui permette d'éviter les collisions.

      Ne pas stocker dans des fichiers parce que c'est local, préférer FTP & co? Idem, pas cohérent. Si je stocke dans un fichier "local", ça peut très bien être un montage NFS ou SSHfs, SMB, ou autre. C'est donc bien plus neutre qu'utiliser une base de données, qui elles auront bien souvent leurs p'tites APIs et autres qui diffèrent avec les autres. Ce qui ne veut bien entendu pas dire que les BDD sont à bannir, loin de la, elles ont leur usage.

      Dans l'ensemble, de ce que j'ai compris de ce que tu dis, ces 12 "règles" semblent être bien trop vagues pour être utilisables comme règles.

      Après des gens qui voient ça comme une religion a ne surtout pas blasphémer, c'est parce que l'humain est un stupide singe overclocké

      Selon Tux dans FreedroidRPG, la solution dans ce cas la, c'est de faire boire une bouteille de refroidissant industriel cul-sec à la personne en surchauffe :)

      PS: j'avais un peu plus détaillé, mais le brouteur à lamentablement crash, donc j'ai refait en condensé.
      J'avais notamment un passage pour tancer tous ces foutus daemons qui croient que le double-fork, le fichier de PID et syslog, c'est l'état de l'art… et qui donc forcent à utiliser l'option --debug quand elles la proposent, pour pouvoir tourner dans un runit, daemon-tools, ou autre du même acabit.
      Probablement que les admin du millénaire dernier considéraient que c'est une excellente idée de faire 400 km pour aller rebooter un système, plutôt qu'il ne le fasse de lui-même et essaie d'uploader un rapport sur le problème ou de laisser prendre la main à distance. C'est plus sûr… (et pendant ce temps, un usager est bloqué, possiblement physiquement, possiblement en danger corporel, mais au moins le réseau est en sécurité?)

      • [^] # Re: Explication

        Posté par  (site web personnel) . Évalué à 2.

        mais c'est le seul moyen que je voie qui permette d'éviter les collisions.

        Un unit systemd permet de définir les variables d'env du process. Docker permet de passer la valeur des variables d'env via l'option -e ou --env-file. Un Pod Kubernetes permet de passer la valeur des variables d'environnement and inline, via un ConfigMap, ou via un Secret. Les plateformes type Heroku, les fonctions Netlify ou AWS Lambda, toutes permettent de définir les valeurs des variables d'environnement.

        Dans le cas de systemd, c'est simple : tu fais plusieurs units. Pour docker, c'est un docker run différent. Pour Kubernetes, c'est un Pod différent, etc…

        Et c'est toujours la même application qu'on instancie de manière différente. Ou est le conflit ?

        Ne pas stocker dans des fichiers parce que c'est local, préférer FTP & co? Idem, pas cohérent.

        C'est pas ce que j'ai dit. Et ce n'est pas ce que 12 factors dit. Ce qui est dit c'est qu'il faut déléguer la persistance à un service tier, configurable, et ne pas le hard-coder dans l'application.

        Dans le cas de docker, le système de fichier (si pas de volume), c'est un tmpfs (ou équivalent) qui disparaît entre 2 lancements.

        Pour faire simple, lorsque tu écris ton application, considère qu'elle sera toujours exécuté sur un tmpfs, et rend paramétrable le lieu de stockage pour que l'ops puisse faire pointer ça vers un volume ou autre.

        Je prend l'exemple de postgresql qui utilise la variable d'env PGDATA, que j'utilise avec docker pour le faire pointer vers un volume qui lui sera persisté (sur le disque local pour mon docker-compose de dev, sur un block storage dans le cloud pour kubernetes, etc…).

        Dans l'ensemble, de ce que j'ai compris de ce que tu dis, ces 12 "règles" semblent être bien trop vagues pour être utilisables comme règles.

        Nulle part sur le site il n'est dit que ce sont des "règles". C'est une philosophie, un ensemble de conseil que tu es libre d'adapter à ton besoin.

        Par exemple, pour la numéro 8 (concurrency), j'ai pas de scrupule à utiliser supervisord dans une image docker pour lancer/gérer plusieurs process python (un worker celery, une webapp django), j'utilise le format %(ENV_CELERY_REPLICAS)s et %(ENV_DJANGO_REPLICAS)s pour la valeur de numprocs afin de rendre cela configurable quand même. Ici, 12factor conseille plutôt plusieurs image docker différentes, mais je m'en cogne.

        https://link-society.com - https://kubirds.com

        • [^] # Re: Explication

          Posté par  . Évalué à 2.

          Ah, ok.

          Oui, effectivement, ne pas hard-coder ce qui n'a pas besoin de l'être… bon, c'est un peu le B.A. BA du métier, ça, non? Ne serait-ce que pour le dev lui-même, ça simplifie tellement les choses sur le moyen terme…

          Je veux dire… rendre configurable, faire du code qui fait une chose et la fait bien, documenter ses dépendances, etc etc… normallement un dev qui a, disons, 2-3 ans de métier (donc après ses études) devrais se l'être fait enseigner par les collègues non? Parce que j'imagine que les écoles ne le font pas toutes (la mienne le faisait pas, en même temps les profs… bon, no comment, rien de bon a ressasser ça)

      • [^] # Re: Explication

        Posté par  (Mastodon) . Évalué à 3.

        De quel conflit parles-tu avec les variables d'environnement?

        • [^] # Re: Explication

          Posté par  . Évalué à 2.

          Deux programmes différents utilisant la même variable (même nom, ça peut aller très vite, quand on a plein de programmes) de manière différente.

          • [^] # Re: Explication

            Posté par  (site web personnel) . Évalué à 2.

            Comment tu gères tes variables d'environnement pour avoir des conflits ?

            Comme je l'ai dit dans un autre commentaire, il te suffit de faire plusieurs unit systemd, ou plusieurs docker run, ou plusieurs k8s pods, ou … différents, même un bête script shell comme suit fait l'affaire :

            #!/bin/sh
            
            export FOO=bar
            exec myprog $@

            A quel moment on a un conflit, vraiment ?

            https://link-society.com - https://kubirds.com

            • [^] # Re: Explication

              Posté par  . Évalué à 2.

              Heureusement qu'on peut réécrire les valeurs des variables, manquerait plus que ça!
              Mais bon, c'est exactement la même chose pour la ligne de commande, on peut réécrire ou modifier les paramètres passés de manière tout aussi simple, sinon plus.

              • [^] # Re: Explication

                Posté par  . Évalué à 2.

                Oui mais c'est pas la question, tu parlais de conflit hors les variables d'environnement ne sont pas global. Même si tu te débrouille pour créer une variable d'environnement system wide, tous les systèmes de déploiement que je connais ne donnent que des variables choisis aux services.

                Ce n'est pas "on peut gérer les conflits" mais "ce serait compliqué à générer".

                https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

          • [^] # Re: Explication

            Posté par  (Mastodon) . Évalué à 3.

            C'est justement pour ça qu'on utilise des variables d'environnement, tu peux lancer deux fois le même programme avec les mêmes variables mais des valeurs différentes.

            C'est à ça que servent les environnements…et leurs variables.

    • [^] # Re: Explication

      Posté par  (Mastodon) . Évalué à 5.

      10 - Keep development, staging and production as similar as possible

      Bah oui, on veut éviter le "ça marche sur ma machine pourtant". Si le développeur est capable de tester en condition de production, c'est pas mal de bug trouvés à la source.

      Bah là je suis moyen d'accord…
      Alors staging et production identiques, c'est une évidence, mais les environnements de dev peuvent être bordéliques.
      Justement pour avoir du « ça marche chez moi mais pas en staging », comprendre pourquoi, et savoir dans quelles conditions réelles ton bouzin peut ou pas fonctionner.

      Si par exemple t'as un truc en Python et qu'un des dev a une pile pas très à jour avec encore un python 3.6 antédiluvien, le jour où ça explose chez lui, tu peux dire « minimum requis = python 3.7 ».
      Sinon, bah tu sais pas, et en général, quand on est sérieux, on préfère savoir.

      Ça fonctionne dans l'autre sens aussi, un DEV qui fait du PHP 8.1 au lieu du 7.4 de la prod, et qui a un comportement bizarre, va pouvoir préparer la migration future vers PHP 8 en pré-corrigeant des soucis, ou peut-être simplement prévenir que tel truc va péter le jour où on voudra migrer en PHP 8.

      Il y a moyen de voir apparaître des bugs masqués par telle version d'une dépendance mais pas telle autre, etc.

      Bref, des environnements de dev hétéroclite, ce n'est pas obligatoirement un mal.

      Par contre, ya pas photo, ça n'a aucun sens d'avoir une pré-prod qui ne soit pas autant que possible un clone de la prod.

      • Yth.
      • [^] # Re: Explication

        Posté par  . Évalué à 4.

        Je dirais que ça dépend, si tu as 200 dev, tu n'as pas envie qu'ils perdent tous leur à temps développé sur une version différente que ce qui sera en prod. Et les test de migration, ce sera fait par une équipe spécifique. S'il y a 3 dev, l'équipe spécfique, ce sera les 3.

        Après, ça dépend aussi du produit, s'il est distribué, c'est bien d'avoir des config différentes, si tu maitrise l'environnement de production, tu t'en fous de savoir qu'il ne tourne pas avec python 3.6. Dans l'absolu, c'est intéressant de le savoir mais si la probabilité que ça arrive est très faible, tu n'as pas envie des gens perdent du temps avec ça au lieu de bosser sur autre chose.

        « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

      • [^] # Re: Explication

        Posté par  (site web personnel) . Évalué à 2.

        12factor parle de déploiement.

        Donc quand il parle de l'environnement de développement, il parle plutôt d'un environnement d'intégration.

        Imagine le workflow suivant :

        • la branche main est déployée en continu sur l'environnement de dev, le développeur teste son travail
        • on tag une vX.Y.Z, ce tag est déployé sur la staging, les QA testent l'ensemble de l'application
        • après validation par les QA, la staging est promu en production

        Bien sûr qu'on ne va pas demander au développeur de faire tourner toute l'infra kubernetes sur sa machine. Le développeur est responsable de sa machine.

        Dans mon "ça marche sur ma machine" il fallait comprendre "je comprends pas pourquoi ca fonctionne en staging et pas en prod". Si la prod utilise PHP 7.4, ben tu utilise PHP 7.4 partout.

        Si un développeur se lance la mission de passer en PHP 8, il commence par sa machine, ensuite on upgrade la staging, puis on upgrade la prod. Avec de "l'Infrastructure as Code" ce process d'upgrade (et de rollback en cas de soucis) devient plus simple.

        https://link-society.com - https://kubirds.com

  • # Compassion

    Posté par  . Évalué à 5.

    Merci pour ce journal qui dénonce grave, je vais le montrer à mon collègue qui essaie de me forcer à mutualiser dès que ça dépasse les deux lignes (DRY as fuck !) et qui refuse de lancer des exceptions (parce que c'est """plus résilient""" de retourner None et de crash bien plus tard)

    • [^] # Re: Compassion

      Posté par  . Évalué à 10.

      Trop de DRY peut amener trop d'abstractions et/ou d'indirections. Et lorsqu'il faut maintenir ça des années après, tu es frappé par le principe de Douche Froide.

      Je découvre à l'occasion de ton commentaire qu'il y a un principe opposé à DRY : WET. "We Enjoy Typing" : j'adore !

      • [^] # Re: Compassion

        Posté par  (site web personnel) . Évalué à 10.

        Le concept du WET est surtout d'attendre la 3ième copie de concept avant de se dire de factoriser le code pour éviter de se tromper d'abstraction.

        "La première sécurité est la liberté"

    • [^] # Re: Compassion

      Posté par  (site web personnel, Mastodon) . Évalué à 10.

      La vrai difficulté de DRY c'est de faire la différence entre ce qui est de la vraie répétition (code identique qui va évoluer de la même manière dans un futur prévisible) et de la coïncidence (les codes se ressemblent beaucoup parce que les fonctionnalités se ressemblent, mais rien ne dit que ça va évoluer pareil).

      Le premier cas gagne à être factorisé, pas le second.

      On reconnaît les DRY mal utilisés à la présence de code commun très surchargé par du spécifique.

      La connaissance libre : https://zestedesavoir.com

      • [^] # Re: Compassion

        Posté par  (site web personnel) . Évalué à 8.

        Le DRY n'est pas de la mutualisation de code à la base. C'est de la mutualisation de concept :

        Principe DRY : chaque élément de connaissance doit avoir une représentation UNIQUE, NON AMBIGUË, OFFICIELLE dans un système
        L'exemple typique est la classe "client" qui a des sens différents en marketing ou en finance.

        "La première sécurité est la liberté"

  • # Sinon, pour les logs

    Posté par  (site web personnel) . Évalué à 6.

    Il suffit de garder ça dans le journal, mais d'avoir rsyslog qui va aussi faire le tri et écrire dans un fichier à part vu que c'est ce qui est voulu (et à mon sens, c'est pas une préoccupation de l'appli, mais du déploiement, donc ça doit être en dehors de l'appli).

    Ça me parait plus générique et flexible que d'avoir en effet des fichiers de logs spécifiques pour chaque application.

    • [^] # Re: Sinon, pour les logs

      Posté par  . Évalué à 5.

      Ça me parait plus générique et flexible que d'avoir en effet des fichiers de logs spécifiques pour chaque application.

      Marrant, j'ai l'opinion exactement inverse (sur cet aspect d'avoir un fichier unique).
      Sur le système lui-même, je préfère avoir un processus qui log par processus qui tourne. Comme ça, si l'un des loggers se ramasse, seul ce qu'il faisait est possiblement erroné/corrompu.
      Puis, comme centraliser, extraire des données, tout ça, c'est quand même plus simple dans un flux unique, tu as un autre processus qui, lui, va avoir un accès en lecture seule a tous ces fichiers de logs journalisés, et en faire un truc plus simple à utiliser.

      Autrement dit, chaque instance de daemon à son propre logger, qui est le seul autorisé à écrire dans son dossier de log. D'autres process sont, en revanche, autorisés à y lire (ça, ça serait l'idéal, en vrai j'ai toujours eu la flemme de faire un user par instance de logger, j'avoue, mais au moins c'est techniquement trivial à faire, et ce depuis facile 20 ans).

      Un cas qui m'est arrivé, c'est un daemon qui se met à log BEAUCOUP TROP (sur un système embarqué). Avec un rsyslog, la rotation aurait pourri les logs du système entier (ou alors il aurait fallu une configuration bien capillo-tractée, et je perds assez mes cheveux comme ça).
      Avec un stockage dédié par instance, seule une instance voit ses logs détruits par la rotation. Du coup, en analysant les journaux des autres applications qui causaient avec, on a pu avoir des indices pour reproduire et régler le bug.

      Le regroupage de log pour en faire un truc plus utile au quotidien, c'est la tâche d'un outil tiers, qui ne détruit pas ses sources de données.
      Le jour ou une donnée manque, ça permets d'aller interroger les journaux bruts, avec plus de facilité que s'ils étaient dans un flux unique (puisque par instance).

      On peut évidemment me répondre de remonter toutes les données en vrac. Mais ça génère le problème suivant: quid des connexion à volumétrie limitée, type 20 megs / mois?
      Réponse: on passe en hors-forfait pour rien.

      • [^] # Re: Sinon, pour les logs

        Posté par  . Évalué à 3.

        Sur le système lui-même, je préfère avoir un processus qui log par processus qui tourne.

        En même temps, tu aimes et utilises runit :P

  • # ~~Exégète~~ --> Thuriféraire

    Posté par  (site web personnel) . Évalué à 4.

    Vive l'esprit critique.

    « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

  • # T'inquiète...

    Posté par  . Évalué à -3.

    … C'est juste un ahuri qui a appris un truc à l'école et qui le répète sans réfléchir. C'est comme l'autre gars qui a écrit une fois que goto c'était le mal, et maintenant on retrouve ça dans tous les cours, docs, tutos, etc…

    Alors que goto c'est tellement bien que je fais exprès d'en mettre dans mon code même quand il n'y en a pas besoin (et en plus ça fait parler les mouches).

  • # Merci à tous pour vos retours

    Posté par  (site web personnel) . Évalué à 5.

    Merci pour cette conversation très enrichissante, j'en tire d'une part que nombreux sont ceux pour qui ces principes fonctionnent, et je vais tenter d'y être plus réceptif. J'y vois d'autre part la confirmation que les 12 facteurs s'appliquent plus particulièrement au monde des micro-services, et qu'il faut raison garder dans leur application à d'autres types d'applications.

    Bisous à tous et à bientôt pour un autre journal qui dénonce grave !

    • [^] # Re: Merci à tous pour vos retours

      Posté par  . Évalué à 4.

      Je ne vois pas pour ma part de raisons pour lesquelles cela s'appliquerait plus particulièrement aux micro-services. Simplement, le non respect de ces bonnes pratiques s'avère plus (et très) vite périlleux quand le nombre de services, d'instances et d'interconnexions augmentent. Libre à toi de ne pas suivre ces recommandations, mais remarque que parmi le public assez calé ici, personne ne semble vraiment les remettre en cause, alors sans que ce soit un argument d'autorité, envisage que cela s'applique de façon assez générale à toutes les applications. Ne cédons pas non plus au dogme malgré tout et n'oublions pas qu'il s'agit avant tout de conseils ;)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.