- The Twelve-Factor App est une méthodologie conçue pour exploiter et faire évoluer sur le long terme des applications web et des SaaS, en couvrant à la fois l’automatisation de la configuration, la portabilité, le déploiement cloud et le déploiement continu
- Elle n’est liée ni à un langage de programmation précis ni à une combinaison donnée de services annexes comme une base de données, une file de messages ou un cache mémoire, ce qui la rend applicable à divers types d’applications de service
- Elle s’appuie sur une expérience acquise en participant directement au développement et au déploiement de centaines d’applications sur la plateforme Heroku, ainsi qu’en observant indirectement le développement, l’exploitation et la montée en charge de plusieurs centaines de milliers d’applications
- L’idée centrale est de fournir un vocabulaire commun pour réduire les coûts de collaboration et l’érosion logicielle qui apparaissent lorsqu’une application croît de manière organique
- Elle peut servir de référence pratique non seulement aux développeurs qui créent des applications de service, mais aussi aux ingénieurs d’exploitation chargés de les déployer et de les administrer
12 principes d’exploitation pour les applications SaaS
- Les logiciels modernes sont souvent proposés sous la forme d’applications web ou de SaaS, et The Twelve-Factor App est une méthodologie destinée à construire ce type d’applications
- L’objectif est de rendre le développement, le déploiement et l’exploitation des applications plus prévisibles
- utiliser une automatisation déclarative de la configuration pour réduire le temps et le coût d’intégration des nouveaux développeurs au projet
- établir un contrat clair avec le système d’exploitation sous-jacent afin d’améliorer la portabilité entre environnements d’exécution
- concevoir l’application de manière à réduire la charge liée aux serveurs et à l’administration système, et à s’adapter au déploiement sur les plateformes cloud modernes
- réduire l’écart entre l’environnement de développement et l’environnement de production afin de permettre le déploiement continu
- permettre la montée en charge sans modifier en profondeur les outils, l’architecture ou les pratiques de développement
- Le champ d’application n’est pas limité à une pile technologique particulière
- elle peut s’appliquer à toute application, quel que soit le langage de programmation utilisé
- les services annexes incluent les bases de données, les files de messages, les caches mémoire, etc.
Un cadre issu de l’expérience Heroku
- Les contributeurs ont participé directement au développement et au déploiement de centaines d’applications sur la plateforme Heroku, et ont observé indirectement le développement, l’exploitation et la montée en charge de plusieurs centaines de milliers d’applications
- Ils ont formalisé les pratiques idéales de développement applicatif à partir de l’expérience et des observations recueillies sur de véritables applications SaaS
- une attention particulière est portée à la manière dont les applications croissent de manière organique au fil du temps
- le texte traite de la collaboration de plusieurs développeurs sur une même base de code
- l’évitement du coût de l’érosion logicielle est considéré comme un objectif important
- La forme s’inspire de Patterns of Enterprise Application Architecture et de Refactoring de Martin Fowler
Les 12 facteurs
- I. Codebase : une base de code versionnée unique, avec plusieurs déploiements
- II. Dependencies : déclarer et isoler explicitement les dépendances
- III. Config : stocker la configuration dans l’environnement
- IV. Backing services : traiter les services annexes comme des ressources attachées
- V. Build, release, run : séparer strictement les étapes de build, de release et d’exécution
- VI. Processes : exécuter l’application sous forme d’un ou plusieurs processus sans état
- VII. Port binding : exposer les services via le port binding
- VIII. Concurrency : faire du scale-out via le modèle de processus
- IX. Disposability : améliorer la robustesse grâce à un démarrage rapide et un arrêt propre
- X. Dev/prod parity : garder le développement, la préproduction et la production aussi proches que possible
- XI. Logs : traiter les logs comme un flux d’événements
- XII. Admin processes : exécuter les tâches d’administration sous forme de processus ponctuels
1 commentaires
Avis sur Hacker News
12-Factor App est une recommandation élaborée en 2011 en s’appuyant davantage sur Heroku et sur les limites de l’infrastructure conteneurisée de l’époque, et ne ressemble pas à un document profondément fondé sur des principes d’ingénierie.
Par exemple, l’idée de mettre la configuration dans des variables d’environnement vient du fait que les auteurs travaillaient chez Heroku, et que Heroku remplissait les variables d’environnement via des champs de saisie dans l’interface des apps web.
Si l’on veut suivre l’historique de la configuration avec le versioning, utiliser GitOps, utiliser des ConfigMap k8s, ou placer des fichiers de configuration sur un volume monté, ce sont globalement de bons choix. Parce qu’ils séparent l’état de la configuration de l’état de déploiement de l’application.
Ce document confond la forêt et les arbres, et recommande surtout des pratiques alignées sur les fonctionnalités produit de l’entreprise qui l’a rédigé plutôt que sur de vrais principes d’ingénierie ; je le considère donc comme un guide nocif.
Si l’on veut stocker de la configuration de manière sûre dans Kubernetes, on finit par utiliser des Secrets, qui sont alors sous forme clé-valeur comme les variables d’environnement. Pour obtenir une sécurité similaire dans Git, il faut une couche de chiffrement, ce qui casse les diffs et nécessite des outils supplémentaires.
Au final, on revient à la raison pour laquelle des outils de déploiement de haut niveau comme Heroku ont été créés.
Le principe selon lequel les logs doivent être traités comme des flux reste valable. Il suffit d’écrire les logs sur STDOUT plutôt que dans un fichier, et de laisser l’orchestrateur les lire et les stocker.
La configuration vient de l’environnement. Selon le mode de déploiement, les apps ont tendance à lire leur configuration depuis différentes sources : un .env en local, un coffre de secrets en production, etc.
Même chose pour le port binding : l’application ouvre un port, puis on place quelque chose comme nginx devant pour configurer un reverse proxy. Les services et ingress K8S jouent ce rôle.
La plus grande critique que l’on puisse faire aujourd’hui à 12-Factor, c’est que le document n’est pas très bien rédigé en pratique et qu’il suppose que le lecteur sait déjà exactement de quoi il parle.
Cela dit, il faut reconnaître que Heroku a popularisé ce concept et en a diffusé l’usage.
Ce n’est pas ajouter un settings.json à la machine avant de lancer l’application ; c’est plutôt que, si le même code source est déployé sur un cluster AKS dans Azure EU north, il utilise les valeurs configurées dans ce cluster, et s’il est déployé sur un cluster Docker Swarm tournant sur des RPi Zero dans un cadre Ikea, il utilise la configuration de ce cluster.
Ce cluster s’appelle Gibson.
J’ai l’impression que chacun de ces points peut être réfuté de manière assez raisonnable.
Premièrement, le principe « une application, un dépôt » n’est pas fondamentalement faux. Il n’y a pas de problème à développer dans un même dépôt plusieurs applications fortement couplées fonctionnellement et partageant un cycle de release, mais qui doivent être déployées séparément afin de bénéficier des avantages de processus distincts et d’un scaling indépendant. Je pense par exemple à la séparation entre une API publique et des workers, comme Sidekiq en Ruby, Celery en Python, ou des consommateurs Kafka classiques.
Deuxièmement, l’idée selon laquelle « une app 12-Factor ne dépend pas de l’existence implicite de packages installés globalement sur le système » est très difficile à atteindre en pratique sans utiliser quelque chose comme Nix. Les dépendances à l’API des appels système du noyau fuient aussi, et la plupart des apps Rust dépendent implicitement de glibc, sauf avec musl. En dehors des distributions slim comme Alpine, elle est présente dans les principales distributions Linux. Je vois aussi Docker comme une béquille nécessaire pour atténuer ce problème.
Troisièmement, stocker la configuration dans des variables d’environnement me semble plus fragile. Forcer les secrets à être placés dans l’environnement peut dégrader la sécurité, et cela fait renoncer à une configuration structurée dans des fichiers, avec sûreté de typage, autocomplétion IDE et parsing automatique. Avec les variables d’environnement, il faut implémenter soi-même des parseurs pour les valeurs de configuration complexes qui ne sont pas de simples chaînes. En pratique, elles sont aussi souvent stockées sous forme de fichiers .env committés dans le dépôt, ce qui rend également caduc l’argument de la sécurité des commits.
Cela ne veut pas dire qu’il ne faut pas dépendre du système d’exploitation, y compris glibc, mais qu’il ne faut pas rendre l’application dépendante de la présence sur la machine d’un package installé par un gestionnaire de paquets propre à un langage.
Je suis d’accord avec le reste des remarques. En particulier, l’histoire des variables d’environnement ressemble moins à un conseil réellement fondé qu’à une pratique courante du développement Ruby de l’époque que les auteurs ont supposée être la meilleure.
Il faut donc se préparer à cette situation, et pour tester facilement différentes combinaisons de versions, je pense que des dépôts séparés sont préférables.
La plupart des langages de haut niveau ne dépendent pas d’une glibc spécifique. Si le runtime du langage fonctionne correctement, l’application fonctionne dessus. Bien sûr, dans certains cas, on finit par utiliser quelque chose comme Docker. Le fait que ce soit difficile ne signifie pas que cela n’a pas de valeur.
Les langages dynamiques ou managés permettent souvent d’ignorer la plupart des tracas liés aux packages système.
Globalement, j’aime bien, mais des non-techniciens ou des personnes à moitié techniques ont tellement souvent dégainé le « 12 factor » comme un carton jaune universel pour retarder des releases que j’ai fini par l’ignorer presque complètement
En réalité, « agile » a connu un sort similaire. Je comprends l’intention de ce genre de lignes directrices, mais leur valeur réelle semble bien plus utile aux personnes qui ne peuvent offrir qu’un leadership technique de tour d’ivoire
J’ai déjà vu des ingénieurs juniors trop zélés ou des aspirants architectes utiliser l’article sur le 12-Factor comme une exigence indispensable pour chaque mise en production
Ce sont de bons objectifs à poursuivre, mais il faut expliquer fermement et systématiquement que, dans la réalité, il faut faire des compromis pour sortir une release et choisir quels aspects ralentir ou repousser
On ne va pas bloquer une release pour de petits écarts, mais si l’ensemble n’est pas aligné, il faut au minimum le traiter comme de la dette technique. Si une release régresse fortement sur un point, il est légitime de la bloquer, ou au moins d’imposer un examen plus approfondi des raisons pour lesquelles ce compromis a été jugé valable
Si une organisation a adopté 12FA comme bonne pratique, il faut s’y conformer, mais cela ne doit pas bloquer le déploiement
12FA n’est pas une case unique à cocher. À mesure que le produit mûrit, on peut, et dans la plupart des cas on devrait, l’implémenter en ajoutant chaque élément un par un au produit, voire en les découpant davantage si nécessaire
Si une bonne ingénierie a été faite au départ, c’est-à-dire avec des abstractions et interfaces appropriées sans tout hardcoder, cela ne devrait pas poser problème
YAGNI est tout autant détourné que 12FA. Ce qui manque le plus à 12FA, ce sont des exemples concrets auxquels les ingénieurs juniors pourraient se référer
J’ai appris à considérer ce genre de problèmes de bonne foi. Il faut comprendre pourquoi la question est soulevée, si le processus existant est flou, défectueux ou manque de fiabilité
Si des préoccupations raisonnables apparaissent, il suffit de modifier le processus et de mettre la documentation à jour
The Twelve-Factor App recommande d’utiliser l’environnement pour la configuration, tandis que Docker dit de ne pas utiliser l’environnement pour la configuration, parce que ce n’est pas sûr
J’aime et j’utilise beaucoup de patterns du 12-Factor, mais certains ont été pensés dans un contexte VPS. À l’époque, l’environnement était stable, sûr et plus figé, alors qu’avec les conteneurs, l’environnement peut se retrouver embarqué dans n’importe quelle couche
À l’ère des conteneurs, ce point précis a été une vraie source de réflexion. Les Docker secrets ne conviennent pas toujours non plus, et il faut parfois pas mal d’acrobaties pour que ça marche
Injecter des secrets dans l’application via des variables d’environnement ne veut pas dire que tout le traitement autour est lui aussi non sécurisé. Par exemple, les conteneurs AWS ECS ont une prise en charge intégrée pour récupérer des secrets depuis Secret Manager au démarrage et les transmettre sous forme de variables d’environnement : https://docs.aws.amazon.com/AmazonECS/latest/developerguide/...
Les secrets sont récupérés au démarrage du conteneur, depuis Secret Manager, avec les identifiants IAM de l’application en cours d’exécution. Il faut donc disposer des droits sur ces secrets
L’intérêt des variables d’environnement semble donc dépendre entièrement de la manière dont elles sont finalement définies. Avec ce type de mécanisme, je ne vois pas de gros inconvénient
Le principal inconvénient visible est qu’un malware générique cherchant à dumper les variables d’environnement peut capturer les valeurs, mais si l’on ne va pas jusqu’à éviter de stocker les secrets durablement en mémoire, se protéger contre cette menace relève surtout de l’obfuscation
Il faut vraiment éviter que des clés privées soient exposées de cette manière
Monter des secrets dans le système de fichiers avec k8s ne me pose pas vraiment de problème. Bien sûr, tout cela dépend de l’environnement de déploiement
Parfois, les variables d’environnement sont l’option la moins mauvaise. Les secrets sont toujours difficiles par nature
Faire aveuglément confiance à une entrée au motif qu’on est en mono-tenant conduit, si les conditions changent plus tard à cause d’un virage métier arbitraire, à des bugs difficiles à retracer et à de longues nuits
Ces dernières années, j’ai beaucoup discuté des apps 12-Factor et j’ai aussi vu beaucoup de confusion. Le site 12-Factor est excellent, mais il s’adresse surtout aux personnes qui comprennent déjà pourquoi ces points sont importants.
Pour celles qui ne connaissent pas les raisons derrière les règles, il fallait une explication plus approfondie. J’ai donc réalisé la vidéo « What are 12 Factor Apps and Why Should You Care? »[1], et plusieurs entreprises m’ont dit qu’elle leur avait été très utile pour former leurs nouvelles recrues ingénieurs/DevOps.
Quel que soit l’endroit où on l’apprend, cela vaut la peine de consacrer une ou deux heures à étudier les apps 12-Factor. La plupart des « règles » sont des choses qui demandent une prise de conscience, et elles ne sont pas immédiatement évidentes tant qu’on n’a pas fait soi-même des erreurs et souffert de leurs conséquences.
[1] https://youtu.be/REbM4BDeua0
Le concept de « tests unitaires » a commencé à gagner en traction vers la fin des années 90, et j’ai été soulagé qu’une personne faisant autorité le défende.
Si je ne l’ai pas fait moi-même avant que Kent Beck ne publie JUnit, c’est parce que le code sur lequel je travaillais n’était pas structuré de façon à pouvoir être facilement piloté par un autre code. Variables globales, état disséminé partout, dépendances à des systèmes externes et à une disposition précise du système de fichiers, absence de modularité : rien ne pouvait être exécuté en dehors du contexte pour lequel il avait été conçu.
Tout cela relevait d’une « mauvaise conception », mais les délais étaient tenus, donc tout le monde faisait ainsi. J’espérais que, si les tests unitaires gagnaient en traction, les programmeurs s’éloigneraient des conceptions monolithiques.
Après 25 ans à voir des tests unitaires pour des getter/setter et un énorme test unitaire qui crée une base de données en mémoire parce que chaque fonction de l’app a besoin d’une base de données live rien que pour s’exécuter, puis finit par échouer et être commenté, j’ai perdu la conviction que les tests unitaires deviendraient autre chose qu’une case à cocher vide de sens. Tout le monde la coche parce que c’est une « bonne pratique », sans jamais s’arrêter pour réfléchir à pourquoi on le fait.
Je n’ai jamais vraiment été d’accord avec les conseils sur la configuration. La configuration finit par être définie par plusieurs acteurs, souvent aussi par les développeurs, et il est donc souvent préférable d’embarquer des valeurs par défaut raisonnables avec l’application, puis de les surcharger via des fichiers propres à chaque environnement et des variables d’environnement.
Pour la plupart des applications côté serveur, c’est l’approche la plus flexible. On sait souvent déjà quelle doit être une partie de la configuration, et il vaut mieux qu’elle soit versionnée, tandis que les secrets doivent être injectés à l’exécution.
Pour éviter de passer énormément de temps à configurer le développement, les tests et la production, la configuration a besoin de surcharges hiérarchiques.
Si elles sont prévues pour le développement, elles finiront par casser la production ; et des valeurs par défaut de production peuvent n’avoir aucun sens en développement.
Une façon de penser la section configuration est : « cette stratégie de configuration fonctionne-t-elle bien avec les conteneurs ? ». Quand on construit une image, on obtient un état disque statique dont les changements ne persistent pas, sauf à créer une nouvelle image.
Si la configuration repose uniquement sur des fichiers, il faut construire une toute nouvelle image pour basculer entre le comportement de test et celui de production.
Pouvoir modifier la configuration indépendamment du disque de base aide à isoler les changements. Il faut pouvoir distinguer si l’app est cassée parce que le déploiement, c’est-à-dire la création de l’image, est cassé, ou parce que la configuration est incorrecte.
En séparant la création de l’image des changements de configuration, cette question disparaît.
Par exemple, il ne faut pas permettre l’erreur humaine tout à fait prévisible consistant, depuis la QA, à envoyer 100 000 tentatives de refus d’authentification vers la file de soumission des tâches de production.
Cela dit, beaucoup de configuration ne concerne pas l’infrastructure, mais plutôt des choses comme le bean ResolverStrategy à câbler dans chaque environnement.
La configuration doit être versionnée, mais gérée séparément du code source. En effet, la configuration ne décrit pas l’image utilisée pour le déploiement, mais le déploiement lui-même.
C’était clairement une norme d’ingénierie influente. Il existe aujourd’hui beaucoup d’abstractions simples pour l’hébergement, comme Render ou Vercel, mais il est un peu étrange de se rappeler que ce document a été écrit en 2012 et qu’à l’époque, les apps web ressemblaient beaucoup plus à un Far West en matière de pratiques communes acceptées.
Ce qui manque largement dans ce document, c’est la justification des règles. Presque tout n’est que règles.
Il est difficile de juger si elles sont bonnes, et ce document n’aide pas à le déterminer.
En voyant seulement le titre, j’ai cru que c’était un commentaire sur l’authentification à deux facteurs. Du genre une app qui exige, pour une seule connexion, une photo de passeport, un scan du visage, un permis de conduire, un SMS, Google Authenticator, un lien par e-mail, un mot de passe et une empreinte digitale.
Aux débuts de Docker, j’ai fait pas mal de travail pour faire en sorte que WordPress se comporte comme une Twelve-Factor App.
Traditionnellement, WordPress ne fonctionnait pas comme ça, et c’est assez compréhensible. WordPress a grandi dans un monde où les serveurs longue durée, dotés d’un disque local persistant et accessible en écriture, étaient courants.
Les choses ont probablement beaucoup changé depuis. C’était vers 2016, mais c’était un défi vraiment intéressant.
J’ai été surpris de voir à quel point cela simplifiait les choses, et on pouvait répartir la charge sur plusieurs nœuds sans se soucier de l’affinité de session.
Bien sûr, cela rendait d’autres aspects plus difficiles, comme le besoin d’une base de données distincte pour les sessions.