Cloud computing

Cloud-native et Twelve-Factor App : ce que chaque facteur implique pour un conteneur

Intermédiaire12 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

La méthodologie twelve-factor reste la description utile la plus courte d'une application qu'un orchestrateur sait exécuter. Ce guide traduit chaque facteur en décisions concrètes pour un service Spring Boot conteneurisé, montre le code de configuration et de cycle de vie qui les met en œuvre, puis aborde les quatre domaines — secrets, observabilité, santé, résilience — où une checklist de 2011 ne suffit plus.

Ce que vous allez apprendre

  • Expliquer chacun des douze facteurs, ce qu'il interdit autant que ce qu'il exige
  • Externaliser correctement la configuration et les secrets d'un service conteneurisé
  • Rendre un processus stateless et jetable, que l'on peut tuer à tout instant sans risque
  • Traiter les logs comme un flux d'événements et cesser d'écrire dans des fichiers à l'intérieur des conteneurs
  • Repérer les lacunes du modèle twelve-factor et ce qu'il faut y ajouter

La méthodologie twelve-factor a été publiée en 2011 par des ingénieurs d'une entreprise de platform-as-a-service qui avaient vu des milliers d'applications prospérer ou s'effondrer sur leur infrastructure. C'est une liste des propriétés dont une application a besoin pour être démarrée, arrêtée, répliquée et déplacée par autre chose qu'un humain. Quinze ans plus tard, les plateformes ont complètement changé et la liste a à peine pris une ride, parce que la question de fond est restée la même : un orchestrateur peut-il lancer davantage de copies, en tuer une au hasard et les déplacer sur une autre machine sans que personne ne s'en aperçoive ?

Ce guide reprend chaque facteur et se demande ce qu'il implique concrètement pour un service conteneurisé — les exemples supposent une application Spring Boot sur Kubernetes, mais rien ici n'est propre à un framework. Le dernier tiers est consacré à ce que le document d'origine ne couvre pas, car un service qui respecte les douze facteurs et rien d'autre n'est pas prêt pour la production en 2026.

Base de code, dépendances, configuration

I. Base de code — une seule base de code suivie en gestion de versions, de nombreux déploiements. Une application correspond à un dépôt. Le même commit est déployé en staging et en production ; ce qui les distingue, c'est la configuration, jamais le code. Deux applications qui partagent une base de code, c'est un monolithe distribué en puissance ; une application éclatée sur deux dépôts ne peut pas être livrée de façon atomique. Le code partagé a sa place dans une bibliothèque avec son propre numéro de version.

II. Dépendances — déclarez-les et isolez-les. Chaque dépendance est listée explicitement avec sa version, et rien n'est supposé présent sur l'hôte. Concrètement : un fichier de build qui fige les versions, un lock file ou des métadonnées de vérification des dépendances commités à côté, et une image de conteneur qui embarque le runtime au lieu d'en attendre un. L'isolation compte autant que la déclaration : un build qui ne réussit que parce qu'un outil se trouve installé sur la machine du développeur échouera dans le pipeline, et une image qui hérite de latest d'une image de base n'est pas reproductible.

III. Config — stockez la configuration dans l'environnement. Le test proposé par le document d'origine reste le meilleur : pourriez-vous rendre le dépôt open source dès maintenant sans divulguer le moindre identifiant ? La configuration, c'est tout ce qui varie d'un déploiement à l'autre — URL de base de données, identifiants, feature flags, adresse d'un service amont, niveaux de log. Elle n'inclut pas ce qui est identique partout, comme les tables de routage internes, qui relèvent du code.

Pour un service Spring Boot, cela revient à s'appuyer sur l'ordre standard de résolution des propriétés et à laisser la plateforme fournir les valeurs :

# application.yaml — defaults only, no environment values, no secrets
spring:
  application:
    name: orders-api
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: none
server:
  shutdown: graceful
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

Évitez le piège qui consiste à coder les environnements dans l'artefact. Un profil prod dans l'image signifie que l'image sait désormais dans quel environnement elle tourne, et la propriété « un seul build, de nombreux déploiements » disparaît. Les profils sont acceptables pour des variantes de comportement (kubernetes, local) ; les valeurs propres à un environnement doivent rester à l'extérieur.

Services externes, build/release/run, processus

IV. Services externes — traitez-les comme des ressources rattachées. Une base de données, un cache, un broker de messages, un relais SMTP et une API tierce sont tous de la même nature : quelque chose de joignable à une URL avec des identifiants. Remplacer un PostgreSQL géré en local par un PostgreSQL managé doit être un changement de configuration, et rien de plus. Corollaire : votre code ne doit pas traiter différemment une dépendance selon qu'elle est locale ou non, ni embarquer d'hypothèses comme un chemin de système de fichiers partagé avec l'hôte de la base.

V. Build, release, run — séparez strictement les étapes. Le build transforme le source en artefact immuable (une image de conteneur, identifiée par son digest). La release associe cet artefact à un jeu de configuration précis et reçoit son propre identifiant. Le run exécute une release. Deux règles en découlent : les releases sont immuables — on ne patche jamais un conteneur en cours d'exécution, on crée une nouvelle release — et chaque release est adressable individuellement, ce qui fait du rollback une opération d'une ligne. Modifier un fichier dans un pod en cours d'exécution casse ce principe, et la modification disparaît de toute façon au redémarrage suivant.

VI. Processus — exécutez l'application sous forme d'un ou plusieurs processus sans état et share-nothing. Tout ce qu'un processus garde en mémoire ou sur le disque local doit être considéré comme un cache susceptible de disparaître à tout moment. Les coupables habituels : l'état de session HTTP, les compteurs de rate limiting en mémoire, les fichiers uploadés écrits dans un répertoire local, et les tâches planifiées qui supposent qu'une seule instance tourne.

Les correctifs sont bien connus : placez l'état de session dans Redis ou faites de la session un jeton signé, utilisez un stockage partagé pour les compteurs, écrivez les uploads dans un object storage, et coordonnez les traitements planifiés avec un verrou distribué ou un ordonnanceur qui garantit une exécution unique. Les sticky sessions sont un contournement, pas une solution — elles empêchent une répartition de charge équilibrée et transforment l'éviction d'un pod en déconnexion visible par l'utilisateur.

Liaison de port, concurrence, processus jetables

VII. Liaison de port — exposez les services via un port. L'application est autonome et écoute sur le port qu'on lui indique ; elle n'est pas déployée dans un serveur d'applications externe qui fournirait le runtime HTTP. Le serveur embarqué de Spring Boot fait exactement cela, et c'est pourquoi le modèle du JAR exécutable a supplanté le déploiement de WAR pour les charges cloud. Un processus twelve-factor peut aussi être un service externe pour un autre processus, joignable à une adresse et un port.

VIII. Concurrence — montez en charge via le modèle de processus. On monte en charge en lançant davantage de processus, pas en faisant grossir un seul. Les différents types de charge deviennent des types de processus distincts qui scalent indépendamment : un type web dimensionné selon le débit de requêtes, un type worker dimensionné selon la profondeur de file, et jamais les deux dans un même déploiement partageant un pool de threads. Le scaling vertical a un plafond et n'améliore pas la disponibilité ; le scaling horizontal améliore les deux, à condition que le facteur VI soit respecté.

IX. Jetable — maximisez la robustesse par un démarrage rapide et un arrêt gracieux. C'est le facteur le plus souvent enfreint en pratique, et il porte entièrement sur le comportement de cycle de vie.

Un démarrage rapide compte parce qu'il borne la vitesse à laquelle la plateforme peut réagir à la charge ou replanifier après la panne d'un nœud. Une initialisation de contexte lente, un préchauffage anticipé du pool de connexions et le scan du classpath l'allongent ; le class-data sharing, la compilation ahead-of-time et l'élagage de l'autoconfiguration sont les leviers habituels.

L'arrêt gracieux compte plus encore. Quand un conteneur est arrêté, la plateforme envoie SIGTERM, attend un délai de grâce, puis envoie SIGKILL. L'application doit cesser d'accepter de nouvelles tâches, terminer ce qu'elle a en cours, fermer ses connexions et sortir avant l'expiration du délai. Sur Kubernetes, une subtilité dans l'ordre des opérations sème beaucoup de confusion : la suppression du pod et le retrait de ses endpoints se font en parallèle, si bien qu'un pod peut recevoir des requêtes pendant un court instant après SIGTERM. Un bref sleep en pre-stop laisse à la couche de routage le temps de se mettre à jour.

spec:
  terminationGracePeriodSeconds: 45
  containers:
    - name: orders-api
      image: registry.example.com/orders-api@sha256:9f2c...
      lifecycle:
        preStop:
          exec:
            command: ["sh", "-c", "sleep 5"]
      readinessProbe:
        httpGet: { path: /actuator/health/readiness, port: 8080 }
        periodSeconds: 5
      livenessProbe:
        httpGet: { path: /actuator/health/liveness, port: 8080 }
        periodSeconds: 10
        failureThreshold: 3

Ce facteur exige aussi que les workers résistent à une mort soudaine. Si un job extrait d'une file est perdu quand le processus meurt, il faut configurer le visibility timeout et la sémantique de redélivrance de la file, et le handler doit être idempotent, car la redélivrance aura lieu.

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/orders-api.jar app.jar
USER 1000
EXPOSE 8080
# exec form: the JVM becomes PID 1 and receives SIGTERM directly
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]

La forme shell d'ENTRYPOINT enveloppe le processus dans /bin/sh, qui ne relaie pas les signaux ; la JVM ne voit alors jamais SIGTERM et se fait tuer brutalement à la fin du délai de grâce. C'est une erreur d'un seul mot qui désactive silencieusement l'arrêt gracieux, et elle est extrêmement répandue. Pour plus de détails sur le packaging des conteneurs, voir les fondamentaux de Docker.

Parité, logs, processus d'administration

X. Parité dev/prod — gardez le développement, le staging et la production aussi proches que possible. Le document d'origine nomme trois écarts : le temps (combien de temps entre l'écriture du code et sa mise en production), les personnes (l'auteur déploie-t-il lui-même) et les outils (les services externes diffèrent-ils). Remplacer le moteur de production par une base en mémoire est l'écart d'outillage classique, et il masque tout ce qui est intéressant — différences de dialecte, comportement des niveaux d'isolation, utilisation des index. Les conteneurs ont largement réglé la question : faites tourner le vrai moteur en local et utilisez la même image partout.

XI. Logs — traitez les logs comme des flux d'événements. L'application écrit sur la sortie standard, sans buffer, et ne sait rien des fichiers, de la rotation ni des destinations. La plateforme collecte le flux et l'achemine vers un système d'agrégation de logs. Écrire dans un fichier à l'intérieur d'un conteneur, c'est perdre les logs avec le conteneur et remplir la couche inscriptible jusqu'à saturation.

Émettez du JSON structuré avec des identifiants de corrélation, pour que l'agrégateur puisse indexer des champs au lieu de parser du texte :

{"timestamp":"2026-09-07T09:14:22.118Z","level":"WARN","service":"orders-api",
 "traceId":"4bf92f3577b34da6a3ce929d0e0e4736","spanId":"00f067aa0ba902b7",
 "message":"payment authorisation retried","orderId":"A-91245","attempt":2}

XII. Processus d'administration — exécutez les tâches ponctuelles comme des processus ponctuels. Migrations de base de données, corrections de données et génération de rapports s'exécutent contre la même release, depuis la même base de code, avec la même configuration — sous forme de job sur la plateforme, pas via une session SSH sur un hôte. Un outil de migration de schéma lancé au démarrage ou en init container respecte ce principe ; un document de requêtes SQL que quelqu'un exécute à la main, non, car il n'est ni versionné, ni relu, ni reproductible.

Ce que les douze facteurs ne couvrent pas

Respecter les douze facteurs permet à une application d'être orchestrée de façon fiable. Cela ne la rend pas exploitable. Quatre lacunes sont assez importantes pour être traitées comme des facteurs supplémentaires.

Les secrets ne sont pas de la simple config. Le facteur III met dans le même sac un feature flag et un mot de passe de base de données, et cela a mal vieilli. Les secrets exigent une rotation sans redéploiement, une piste d'audit des accès, un chiffrement au repos avec une clé que vous maîtrisez et un rayon d'impact limité à une seule charge de travail. Le modèle qui satisfait tout cela : la charge de travail s'authentifie auprès de la plateforme avec une identité qu'elle n'a pas eu à stocker — un rôle IAM ou une managed identity — et récupère ses secrets dans un coffre managé au démarrage, puis les rafraîchit périodiquement. Rien de sensible ne se trouve dans l'image, le manifeste, le dépôt ou le bloc d'environnement.

L'observabilité ne se limite pas aux logs. Le facteur XI couvre l'un des trois signaux. Un service exploitable émet aussi des métriques (débit, erreurs, durée, saturation, plus des compteurs métier) et des traces distribuées dont le contexte se propage d'un service à l'autre. OpenTelemetry est le standard neutre vis-à-vis des fournisseurs pour les trois, et le choix par défaut en pratique ; la mise en place côté application est détaillée dans Actuator et observabilité.

La santé n'est pas un simple booléen. La plateforme a besoin d'au moins trois réponses distinctes : le processus est-il vivant et mérite-t-il d'être conservé (liveness), est-il capable de servir du trafic en ce moment (readiness), et a-t-il terminé son long démarrage initial (startup) ? Confondre liveness et readiness est à l'origine d'un mode de défaillance bien connu : une dépendance temporairement indisponible fait tuer et redémarrer toutes les répliques en boucle, transformant une dégradation partielle en panne totale. La liveness ne doit tester que le processus lui-même.

Résilience et contrats d'API. Timeouts, retries avec jitter, circuit breakers, backpressure et rate limits sont des propriétés d'architecture que la méthodologie ne mentionne jamais, pas plus que l'évolution rétrocompatible des API ou les migrations de schéma qu'on peut exécuter sans risque pendant que l'ancienne et la nouvelle release tournent en même temps. Le pattern de migration expand-then-contract — ajouter une colonne nullable, déployer du code qui écrit dans les deux, faire le backfill, déployer du code qui lit la nouvelle colonne, supprimer l'ancienne — est ce qui rend réellement sûr le rollback instantané du facteur V.

La définition du cloud-native par la CNCF traduit l'ambition plus large : des systèmes faiblement couplés, résilients, gérables et observables, associés à une automatisation qui permet aux ingénieurs d'apporter fréquemment et de manière prévisible des changements à fort impact. Les douze facteurs sont la façon dont un processus individuel gagne sa place dans un tel système ; ils sont le ticket d'entrée, pas la conception complète, et c'est pourquoi ils s'inscrivent aux côtés des préoccupations plus larges de l'architecture microservices.

Un audit rapide pour un service existant

Posez ces questions à un service que vous exploitez déjà. Chaque « non » devient un ticket concret.

  1. Le dépôt pourrait-il être rendu public aujourd'hui sans divulguer d'identifiant ?
  2. L'artefact déployé est-il identifié par un digest immuable, et le même digest tourne-t-il en staging et en production ?
  3. Si deux répliques tournent et que l'une est tuée en pleine requête, un utilisateur perd-il des données ou sa session ?
  4. Le processus s'arrête-t-il proprement dans le délai de grâce après SIGTERM, en terminant les requêtes en cours ?
  5. Le conteneur écrit-il sur le disque local autre chose qu'un véritable cache ?
  6. Les logs partent-ils sur la sortie standard, structurés et porteurs d'un identifiant de trace ?
  7. La liveness et la readiness vérifient-elles réellement des choses différentes ?
  8. Peut-on renouveler un secret sans reconstruire l'image ?
  9. Les migrations s'exécutent-elles comme un job versionné contre la même release, et sont-elles compatibles avec la release précédente ?
  10. Le démarrage est-il assez rapide pour que l'autoscaling réagisse avant que le pic de trafic ne soit passé ?

Points clés à retenir

  • Les douze facteurs décrivent le contrat entre une application et un orchestrateur : une seule base de code, des dépendances déclarées, une configuration externalisée, des services externes rattachés et une séparation stricte entre build, release et run.
  • L'absence d'état et le caractère jetable portent l'essentiel du poids pratique — pas d'état de session local, pas de fichiers locaux, un démarrage rapide et un arrêt gracieux qui reçoit effectivement SIGTERM.
  • La configuration doit rester hors de l'artefact pour qu'une même image immuable tourne sans modification dans chaque environnement ; des profils de build nommés d'après les environnements cassent discrètement cette propriété.
  • Les logs sont un flux structuré, sans buffer, envoyé sur la sortie standard et acheminé par la plateforme, jamais des fichiers à l'intérieur d'un conteneur.
  • La méthodologie est un socle, pas une définition complète : ajoutez des secrets managés avec une identité plateforme, une observabilité complète, des signaux de santé distincts et des stratégies explicites de résilience et de migration.

Sources et lectures complémentaires

Questions fréquentes

La méthodologie twelve-factor est-elle dépassée ?
Les facteurs eux-mêmes ont bien vieilli, car ils décrivent le contrat entre une application et un orchestrateur, et ce contrat n'a pas changé. C'est le périmètre qui a évolué : la méthodologie ne dit presque rien de la gestion des secrets, du tracing distribué, des sondes de santé, des retries, de la découverte de services ni de l'autoscaling, qui sont aujourd'hui le strict minimum. Voyez-la comme un socle nécessaire, pas comme une définition complète du cloud-native.
Comment les secrets entrent-ils dans le facteur config si les variables d'environnement sont risquées ?
Ce que le facteur exige vraiment, c'est que la configuration vive hors du code et varie selon le déploiement, pas qu'elle soit forcément une variable d'environnement. Les variables d'environnement fuient dans les crash dumps, les listes de processus, les processus enfants et les lignes de log, et leur rotation impose un redémarrage. Récupérez les secrets au démarrage depuis un coffre managé en utilisant l'identité de plateforme propre à la charge de travail, ou laissez la plateforme les projeter dans un fichier monté, et mettez-les en cache avec un intervalle de rafraîchissement.
Une base de données ou un broker de messages peut-il être twelve-factor ?
Non, et ce n'est pas nécessaire. La méthodologie décrit des processus applicatifs sans état ; les systèmes avec état enfreignent délibérément plusieurs facteurs, notamment le caractère jetable et la concurrence share-nothing. Dans une architecture twelve-factor, ces systèmes sont des services externes rattachés par URL. Les faire tourner vous-même sur un orchestrateur est possible avec des operators et des volume claims, mais vous exploitez alors une base de données, vous n'écrivez plus une application twelve-factor.
Que signifie la parité dev/prod quand la production compte quarante services ?
Cela signifie que les écarts qui comptent sont comblés : mêmes technologies de services externes et mêmes versions majeures, même image de conteneur, même mécanisme de configuration, et des heures plutôt que des semaines entre l'écriture du code et sa mise en production. Cela ne veut pas dire que chaque développeur fait tourner tout le parc en local, ce qui devient irréaliste au-delà de quelques services. La réponse habituelle : des conteneurs pour les dépendances directes, plus un environnement d'intégration partagé, bon marché et éphémère.