- The Twelve-Factor App es una metodología para operar y escalar apps web y SaaS a largo plazo, y aborda en conjunto la automatización de configuración, la portabilidad, el despliegue en la nube y la entrega continua
- No está atada a una combinación específica de lenguaje de programación ni de servicios de respaldo como bases de datos, colas o cachés en memoria, por lo que puede aplicarse a diversas aplicaciones como servicio
- Se basa en la experiencia de haber participado directamente en el desarrollo y despliegue de cientos de apps en la plataforma Heroku, y de haber observado indirectamente el desarrollo, la operación y el escalado de cientos de miles de apps
- La preocupación central es ofrecer un vocabulario común para reducir los costos de colaboración y la erosión del software que surgen cuando una app crece orgánicamente
- Puede servir como criterio práctico no solo para desarrolladores que crean aplicaciones de servicio, sino también para ingenieros de operaciones que las despliegan y administran
12 principios operativos para apps SaaS
- El software moderno suele ofrecerse como app web o en formato SaaS, y Twelve-Factor App es una metodología para crear este tipo de aplicaciones
- El objetivo es hacer más predecibles los procesos de desarrollo, despliegue y operación de una app
- Usa automatización declarativa de configuración para reducir el tiempo y costo de incorporar a nuevos desarrolladores al proyecto
- Establece un contrato claro con el sistema operativo subyacente para aumentar la portabilidad entre entornos de ejecución
- Está diseñado para reducir la carga de administración de servidores y sistemas, y adaptarse al despliegue en plataformas modernas de nube
- Reduce las diferencias entre el entorno de desarrollo y el de producción para hacer posible la entrega continua
- Permite escalar sin cambiar de forma drástica las herramientas, la arquitectura ni las prácticas de desarrollo
- Su alcance no se limita a un stack tecnológico específico
- Puede aplicarse a apps escritas en cualquier lenguaje de programación
- Los servicios de respaldo incluyen bases de datos, colas, cachés en memoria, etc.
Contexto resumido a partir de la experiencia en Heroku
- Los contribuidores participaron directamente en el desarrollo y despliegue de cientos de apps en la plataforma Heroku, y observaron indirectamente el desarrollo, la operación y el escalado de cientos de miles de apps
- A partir de la experiencia y las observaciones obtenidas en apps SaaS reales, organizaron las prácticas ideales para el desarrollo de aplicaciones
- Pusieron atención en cómo las apps crecen orgánicamente con el tiempo
- Abordan la forma en que varios desarrolladores colaboran sobre una misma base de código
- Consideran evitar el costo de la erosión del software como un objetivo importante
- El formato se inspira en Patterns of Enterprise Application Architecture y Refactoring de Martin Fowler
Los 12 factores
- I. Codebase: una sola base de código bajo control de versiones y múltiples despliegues
- II. Dependencies: declarar y aislar explícitamente las dependencias
- III. Config: almacenar la configuración en el entorno
- IV. Backing services: tratar los servicios de respaldo como recursos conectados
- V. Build, release, run: separar estrictamente las etapas de build y ejecución
- VI. Processes: ejecutar la app como uno o más procesos sin estado
- VII. Port binding: exponer servicios hacia afuera mediante port binding
- VIII. Concurrency: escalar horizontalmente con el modelo de procesos
- IX. Disposability: aumentar la robustez con inicio rápido y apagado elegante
- X. Dev/prod parity: mantener desarrollo, staging y producción lo más parecidos posible
- XI. Logs: tratar los logs como flujos de eventos
- XII. Admin processes: ejecutar tareas administrativas como procesos de una sola vez
1 comentarios
Opiniones en Hacker News
12-Factor App es una recomendación creada en 2011 apoyándose más en Heroku y en las limitaciones de la infraestructura basada en contenedores de la época, no parece un documento profundamente basado en principios de ingeniería.
Por ejemplo, la afirmación de que hay que poner la configuración en variables de entorno se debe a que los autores trabajaban en Heroku, y Heroku llenaba las variables de entorno mediante campos de entrada de una app web.
Si quieres rastrear el historial de la configuración con control de versiones, usar GitOps, usar ConfigMap de k8s o poner archivos de configuración en un volumen montado, en general todas son opciones aceptables. Porque separan el estado de la configuración del estado de despliegue de la app.
Este documento confunde el bosque con los árboles y hace recomendaciones ajustadas a las funciones del producto de la empresa que lo escribió, más que a principios reales de ingeniería; por eso lo veo como una guía dañina.
Si quieres almacenar configuración de forma segura en Kubernetes, al final terminas usando Secrets, y eso toma la forma de clave-valor, igual que las variables de entorno. Para obtener una seguridad similar en Git necesitas una capa de cifrado, y entonces se rompen las comparaciones de diferencias y hacen falta herramientas adicionales.
Al final vuelves a la razón por la que se crearon herramientas de despliegue de alto nivel como Heroku.
El principio de tratar los logs como flujos sigue siendo correcto. Basta con escribir los logs en STDOUT, no en archivos, y dejar que el orquestador los lea y los almacene.
La configuración se toma del entorno. Según el método de despliegue, las apps tienden a leer la configuración desde fuentes distintas: .env en local, un almacén de secretos en producción, etc.
Lo mismo con el enlace de puertos: la app abre un puerto y se pone algo como nginx al frente para configurar un proxy inverso. Los servicios e ingress de K8S cumplen ese rol.
La crítica más fuerte que se le puede hacer hoy a 12-Factor es que el documento en realidad no estaba bien escrito y asume que el lector ya sabe exactamente de qué está hablando.
Aun así, hay que reconocerle a Heroku el mérito de haber difundido ampliamente el concepto y expandido su uso.
No se trata de agregar un settings.json a la máquina antes de iniciar la app, sino de que el mismo código fuente, si se despliega en un clúster AKS de Azure EU north, use los valores configurados en ese clúster; y si se despliega en un clúster RPi Zero Docker Swarm dentro de un marco de Ikea, use la configuración de ese clúster.
Ese clúster se llama Gibson.
Siento que cada uno de estos puntos puede refutarse de forma bastante razonable.
Primero, el principio de un repositorio por app no es fundamentalmente incorrecto. No hay problema en desarrollar en un solo repositorio varias apps que están fuertemente acopladas funcionalmente y comparten ciclo de lanzamiento, pero que deben desplegarse por separado para obtener las ventajas de procesos separados y escalado independiente. Se me vienen a la mente la separación entre API pública y procesos worker, por ejemplo Sidekiq en Ruby, Celery en Python o consumidores comunes de Kafka.
Segundo, decir que “una app 12-Factor no depende de la existencia implícita de paquetes globales del sistema” es algo muy difícil de lograr en la práctica si no usas algo como Nix. También se filtran dependencias de la API de llamadas al sistema del kernel, y la mayoría de las apps Rust, salvo con musl, dependen implícitamente de glibc. Está presente en las principales distribuciones Linux, salvo distribuciones slim como Alpine. Creo que Docker también se necesita como apoyo para mitigar este problema.
Tercero, guardar configuración en variables de entorno parece más frágil. Puede reducir la seguridad al forzar que secretos se pongan en el entorno, y te hace renunciar a configuraciones en archivos estructurados que ofrecen seguridad de tipos, autocompletado en el IDE y parseo automático. En variables de entorno tienes que implementar tú mismo parsers para valores de configuración complejos que no sean cadenas. En la práctica, muchas veces se guardan en forma de archivos .env y se commitean al repositorio, así que el argumento sobre la seguridad de los commits pierde sentido.
No significa que no debas depender del sistema operativo, incluida glibc, sino que no debes hacer que sea obligatorio que cierto paquete de un gestor de paquetes de un lenguaje esté instalado en la máquina.
Estoy de acuerdo con el resto de las observaciones. En especial, lo de las variables de entorno se parece más a que los autores asumieron que una práctica común en el desarrollo Ruby de la época era la mejor opción, y lo escribieron así, que a un consejo con fundamentos reales.
Por eso hay que prepararse para esa situación, y si quieres probar fácilmente combinaciones de versiones distintas, creo que es mejor tener repositorios separados.
La mayoría de los lenguajes de alto nivel no dependen de una glibc específica. Si el runtime del lenguaje funciona correctamente, la app también funciona encima de él. Claro que en algunos casos terminarás usando algo como Docker. Que sea difícil no significa que no tenga valor.
Los lenguajes dinámicos o administrados muchas veces permiten ignorar la mayor parte de las molestias relacionadas con paquetes del sistema.
En general me gusta, pero demasiadas veces alguien no técnico o medio técnico sacó “12 factor” como si fuera una tarjeta amarilla universal para retrasar releases, así que terminé ignorándolo casi por completo.
La verdad, con “agile” pasó algo parecido. Entiendo la intención de estas guías, pero su valor real parece mucho más útil para personas que no pueden ofrecer más que liderazgo técnico de torre de marfil.
Me tocó ver a ingenieros junior demasiado entusiastas o aspirantes a arquitectos usar el artículo de 12-Factor como si fuera un requisito obligatorio para cada lanzamiento.
Hay que explicar de forma firme y consistente que estas cosas son buenos objetivos a perseguir, pero que en la realidad hay que hacer concesiones para lanzar y elegir qué partes se retrasan o se posponen.
No bloquearía un release por una desviación menor, pero si en general no encaja, al menos debería tratarse como deuda técnica. Si un release retrocede mucho en un punto, es justo bloquearlo o, como mínimo, exigir una revisión más detallada de por qué se consideró que esa concesión valía la pena.
Si la organización adoptó 12FA como buena práctica, debería cumplirse, pero no debería bloquear el despliegue.
12FA no es una única casilla que hay que marcar. A medida que el producto madura, cada punto puede implementarse agregándolo al producto uno por uno, y si hace falta dividiéndolo aún más; en la mayoría de los casos así debería ser.
Si al principio se hizo buena ingeniería, es decir, si se usaron abstracciones e interfaces adecuadas y no se hardcodeó todo, no debería ser un problema.
YAGNI se abusa tanto como 12FA. Lo que más le falta a 12FA son ejemplos concretos que puedan servir de referencia a ingenieros junior.
Aprendí a asumir buena fe en estos problemas. Hay que ver por qué se plantea el problema, si el proceso existente no es claro, si tiene fallas o si le falta confiabilidad.
Si se encuentra una preocupación razonable, se cambia el proceso y se actualiza la documentación.
Twelve-Factor App dice que se use el entorno para la configuración, y Docker dice que no se use el entorno para la configuración, porque no es seguro.
Me gustan y uso muchos patrones de 12-Factor, pero algunos se pensaron en el contexto de VPS. En ese momento el entorno era estable, seguro y más fijo, pero en contenedores el entorno puede terminar incluido en alguna capa.
En la era de los contenedores, este punto específico fue bastante problemático. Docker secrets tampoco siempre encaja bien, así que para hacerlo funcionar hacen falta varias acrobacias.
Inyectar secretos en la app como variables de entorno no significa que todo su manejo externo sea inseguro. Por ejemplo, los contenedores de AWS ECS tienen soporte integrado para obtener secretos de Secret Manager al iniciar y pasarlos como variables de entorno: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/...
Los secretos se obtienen al momento de iniciar el contenedor, y se traen de Secret Manager usando las credenciales IAM de la aplicación en ejecución. Por lo tanto, debe tener permisos sobre esos secretos.
La ventaja de usar variables de entorno parece depender totalmente de cómo se configuren al final. Con un mecanismo así, no veo grandes desventajas.
La principal desventaja que veo es que malware común que intente volcar variables de entorno podría capturar los valores, pero si no se evita almacenar secretos de forma permanente en memoria, mitigar esa amenaza suele parecer más bien ofuscación.
De verdad hay que evitar que las claves privadas queden expuestas así.
No tengo mayor problema con montar secretos en el sistema de archivos usando k8s. Por supuesto, todo esto depende del entorno de despliegue.
A veces las variables de entorno son la opción menos mala, porque los secretos siempre son difíciles.
Si confías ciegamente en la entrada porque es single-tenant, cuando más adelante alguna decisión arbitraria del negocio cambie las condiciones, terminarás con bugs difíciles de rastrear y noches largas.
En los últimos años se habló mucho de las apps de 12 factores, y también vi mucha confusión. El sitio de 12-Factor es excelente, pero sirve sobre todo para quienes ya entienden por qué esos puntos son importantes.
Para quienes no conocen las razones detrás de las reglas, hacía falta una explicación más profunda. Por eso hice el video “What are 12 Factor Apps and Why Should You Care?”[1], y supe que a varias empresas les resultó muy útil para capacitar a nuevos ingenieros/DevOps.
Donde sea que lo aprendan, vale la pena dedicar una o dos horas a estudiar las apps de 12 factores. La mayoría de las “reglas” son cosas que requieren tomar conciencia y no resultan obvias de inmediato hasta que uno comete los errores en carne propia y sufre las consecuencias.
[1] https://youtu.be/REbM4BDeua0
El concepto de “pruebas unitarias” empezó a ganar traction hacia fines de los 90, y fue un alivio que alguien con autoridad lo promoviera.
La razón por la que yo no las hacía directamente hasta que Kent Beck publicó JUnit era que el código en el que trabajaba no tenía una estructura adecuada para ser controlado por otro código. Por variables globales, estado ampliamente distribuido, dependencias de sistemas externos y de un layout específico del sistema de archivos, e ignorar la modularidad, nada podía ejecutarse fuera del contexto para el que había sido diseñado.
Todo eso era “mal diseño”, pero cumplía con las fechas de entrega, así que todos lo hacían. Esperaba que, si las pruebas unitarias ganaban traction, los programadores se alejaran de los diseños monolíticos.
Después de 25 años de ver pruebas unitarias para getters/setters y una enorme prueba unitaria que crea una base de datos en memoria porque todas las funciones de la app necesitan una base de datos en vivo solo para intentar ejecutarse, y que finalmente falla y se comenta, perdí la fe en que las pruebas unitarias llegaran a ser algo más que una casilla sin sentido. Todos la completan porque es una “buena práctica”, pero no se detienen a pensar por qué lo hacen.
Siempre estuve más en desacuerdo con el consejo sobre configuración. La configuración termina siendo definida por varios actores, y muchas veces también por desarrolladores, así que con frecuencia lo mejor es incluir valores predeterminados razonables junto con la aplicación y sobrescribirlos con archivos por entorno y variables de entorno.
Para la mayoría de las aplicaciones del lado del servidor, este enfoque es el más flexible. Muchas veces ya se sabe cuál debe ser cierta configuración, y conviene que esté bajo control de versiones, pero los valores secretos deben inyectarse en tiempo de ejecución.
Para no gastar una cantidad enorme de tiempo configurando desarrollo, pruebas y producción, la configuración necesita sobrescritura jerárquica.
Si son de desarrollo, eventualmente romperán producción; y los valores por defecto de producción quizá no tengan ningún sentido en desarrollo.
Una forma de pensar la sección de configuración es: “¿esta estrategia de configuración encaja bien con contenedores?”. Al construir una imagen, se obtiene un estado de disco estático cuyos cambios no persisten a menos que se cree una nueva imagen.
Si la configuración es solo basada en archivos, habría que construir una imagen completamente nueva para alternar entre el comportamiento de pruebas y el de producción.
Permitir que la configuración cambie de forma independiente del disco base ayuda a aislar los cambios. Hay que poder distinguir si la app se rompió porque falló el despliegue, es decir, la creación de la imagen, o porque la configuración estaba mal.
Si separas la creación de la imagen de los cambios de configuración, esa pregunta desaparece.
Por ejemplo, no deberías permitir un error humano totalmente previsible como enviar desde QA 100 mil reintentos de rechazo de autenticación a una cola de envío de trabajos de producción.
Dicho eso, mucha configuración no trata sobre infraestructura, sino sobre cuestiones como qué bean ResolverStrategy conectar en cada entorno.
La configuración debe estar versionada, pero gestionarse por separado del código fuente. Esto se debe a que la configuración no describe la imagen usada para el despliegue, sino el despliegue en sí.
Sin duda fue una norma de ingeniería influyente. Hoy hay muchas abstracciones sencillas de hosting como Render o Vercel, pero se siente un poco extraño si uno recuerda que este documento se escribió en 2012 y que, en términos de prácticas comunes aceptadas, las apps web de esa época eran mucho más como el Lejano Oeste.
Lo que falta en gran medida en este documento es una justificación de las reglas. Casi todo son simplemente reglas.
Es difícil juzgar si las reglas son buenas, y este documento no ayuda a descubrirlo.
Por el título, pensé que era un comentario sobre autenticación de dos factores. Algo como una app que, para iniciar sesión una sola vez, te pide foto del pasaporte, escaneo facial, licencia de conducir, SMS, Google Authenticator, enlace por correo, contraseña y huella digital.
En los primeros días de Docker, trabajé bastante para hacer que WordPress se comportara como una Twelve-Factor App.
Tradicionalmente WordPress no funcionaba así, y hasta cierto punto era lógico. WordPress creció en un mundo donde eran comunes los servidores de larga vida con un disco local escribible y persistente.
Desde entonces seguramente las cosas cambiaron bastante. Hablo de alrededor de 2016, pero fue un desafío realmente divertido.
Me sorprendió cuánto se simplificaban las cosas, y permitió balancear carga entre varios nodos sin preocuparnos por la afinidad de sesión.
Claro que se volvió más difícil en otros aspectos, como necesitar una base de datos separada para sesiones.