Publicado en
Autor
Si estás involucrado en la arquitectura de aplicaciones de software que se ejecutan en contextos de cómputo en la nube, posiblemente estés familiarizado con el término twelve-factor application (aplicación de 12 factores); si no, deberías de estarlo.
Este es un concepto utilizado para describir aquellas aplicaciones que han sido diseñadas y programadas para operar de forma adecuada en la nube. Para lograrlo, deben cumplir 12 requisitos o factores. En este artículo los explico y contextualizo.
¿De dónde vienen los 12 factores?
Como probablemente ya has leído en muchos lados, incluyendo las páginas de SG, hay tres modalidades principales de cómputo en la nube: SaaS (software como servicio), IaaS (infraestructura como servicio) y PaaS (plataforma como servicio). La promesa de la modalidad PaaS es que una vez que has construido tu aplicación, la subes a un PaaS y tu aplicación automáticamente opera desde la nube sin necesidad de que tú tengas que preocuparte por cuestiones de infraestructura tales como mantener el sistema operativo, almacenamiento, configuración de puertos, etcétera. Tú simplemente indicas qué frameworks y servicios requiere tu app, la subes, y la infraestructura se genera y gestiona de forma automática.
El modelo PaaS suena maravilloso, pero para sacarle provecho se requiere que las aplicaciones desarrolladas sean “amigables con la nube”. Una aplicación amigable con la nube no es aquella que simplemente puede ejecutarse en la nube; sino que también escala de forma elástica, no tiene estado (stateless), usa filesystems efímeros, y trata todo como un servicio. Las aplicaciones construidas de esta manera se pueden desplegar y escalar rápidamente.
El problema es que es un modelo nuevo con el que la mayoría de los desarrolladores no tiene experiencia previa, por lo que desconoce las consideraciones arquitectónicas que debe tener al construir aplicaciones de este tipo.
Para resolverlo, un grupo de personas en la empresa Heroku (uno de los proveedores pioneros de PaaS) definió los 12 factores. En esencia, son un manifiesto que describe las reglas y lineamientos que deben seguirse para construir una aplicación nativa a la nube. El objetivo es mostrar cómo construir aplicaciones que estén desacopladas del sistema operativo, se puedan desplegar de forma automatizada, y escalen de forma dinámica.
A continuación describo los factores.
I. Base de código
Una base de código registrada en control de versiones, muchos despliegues.
Cada aplicación debe tener un (y solo un) repositorio de código. El repositorio debe tener control de versiones, y a partir del mismo repositorio se generan las instancias de la aplicación. Para instanciar distintas versiones de una aplicación (ej. producción, staging, desarrollo) se usa el mismo repositorio de código, pero con ramas distintas.
II. Dependencias
Declarar dependencias explícitamente y aislarlas.
La aplicación declara sus dependencias —bibliotecas, componentes externos— de forma explícita y precisa por medio de un manifiesto o especificación. Adicionalmente, se debe utilizar una herramienta para aislar dependencias y de esta forma evitar que se “cuelen” dependencias implícitas provenientes del ambiente de ejecución.
Por ejemplo, en Ruby se puede usar Gem Bundler para declarar dependencias y bundle exec para aislar dependencias. En Python se usa Pip para declaración y Virtualenv para aislamiento. Incluso en C se puede usar Autoconf para declaración y enlaces estáticos (static linking) para aislamiento. No importa cuáles sean las herramientas, la declaración de dependencias siempre se debe usar en conjunto con un mecanismo de aislamiento.
Las aplicaciones tampoco deben depender de herramientas de sistema. Así que una aplicación que invoque por medio de línea de comando cierta herramienta (ej. curl o ImageMagick) está violando este factor. Lo que se debe hacer es incorporar dicha herramienta como una biblioteca y especificarla como dependencia.
III. Configuración
Guardar la configuración en el entorno.
La configuración de una aplicación está relacionada con aquellos parámetros que pueden variar dependiendo del ambiente (producción, pruebas, desarrollo). Esto incluye: urls a otros servicios, contraseñas de base de datos, valores que varían en cada instancia.
Guardar estos valores como constantes en el código es una práctica no segura y que viola los 12 factores. La configuración puede variar entre instancias, pero el código no debe variar. La práctica recomendada consiste en que la configuración se haga disponible a la aplicación por medio de variables de entorno (env vars).
IV. Servicios externos
Tratar a los servicios externos como recursos conectables.
Un servicio externo es cualquier servicio que la app consume por medio de la red como parte de su operación normal. Esto incluye bases de datos, sistemas de mensajería, servidores SMTP para enviar correos, entre otros. Estos servicios algunas veces son locales y operados por los mismos administradores que la aplicación, pero también pueden ser servicios remotos proporcionados por terceros.
La aplicación no debe hacer distinción entre servicios locales y de terceros. Ambos son recursos conectados, accesibles por medio de un url u otras credenciales guardadas en la configuración. Esto permite que sea posible reemplazar el uso de una base de datos local en MySQL por una base de datos remota, como Amazon RDS, cambiando solamente la configuración y sin necesidad de hacer ninguna modificación en el código.
V. Construir, liberar, ejecutar
Separar claramente las fases de construcción y ejecución.
Una base de código se transforma en un despliegue (deploy) por medio de tres fases:
En la fase de construcción (build) se genera un paquete ejecutable a partir de la base de código. El paquete ejecutable puede contener dependencias (bibliotecas), código ejecutable (ya sea binario o interpretado) y activos (ej. Imágenes, hojas de estilo).
La fase de liberación (release) toma el paquete ejecutable y lo combina con una configuración de despliegue. El paquete resultante contiene tanto el build como la configuración y está listo para ejecutarse.
En la fase de ejecución (runtime) se opera la aplicación por medio de la ejecución de un conjunto de procesos de una liberación específica de la aplicación.
Separar estas fases involucra que no se pueden realizar acciones que no correspondan a dicha fase. Por ejemplo, no se puede hacer cambios al código durante la fase de ejecución ya que no hay forma de propagar esos cambios a la fase de construcción.
VI. Procesos
La aplicación se ejecuta en forma de procesos sin estado.
En el escenario más simple, una aplicación consiste de un solo script y el ambiente de ejecución es una computadora personal con el runtime, y el proceso es lanzado por medio de línea de comandos (por ejemplo, python mi_script.py). asdf
La aplicación opera como procesos independientes que no comparten nada entre sí. Cualquier dato que requiera guardarse debe ser almacenado en un servicio externo de persistencia, típicamente una base de datos. El espacio en memoria o sistema de archivos se puede usar como caché temporal para transacciones individuales, pero nada más. La aplicación nunca puede asumir que algo cacheado en memoria o disco estará disponible para una petición futura, ya que no hay ninguna garantía de que una petición futura sea atendida por el mismo proceso. Incluso cuando la aplicación se ejecute en un proceso único en un solo nodo de cómputo, este puede ser reiniciado en cualquier momento. Algunas aplicaciones web utilizan “sesiones pegajosas” (sticky sessions) —es decir, almacenar el estado de la sesión del usuario en memoria del proceso y esperar que las peticiones futuras sean ruteadas al mismo proceso. Esto es una violación de los 12 factores. Para guardar el estado de la sesión del usuario se puede usar un almacén con expiración de tiempo, como Redis o Memcached.
VII. Asignación de puertos
Publicar servicios mediante asignación de puertos.
Tradicionalmente, las aplicaciones web se ejecutan en el contexto de un contenedor web. Por ejemplo, Tomcat en el caso de aplicaciones Java, o Apache httpd + mod-php en el caso de PHP.
En contraste, una aplicación twelve-factor está completamente autocontenida y no depende de un servidor web en el entorno de ejecución. La aplicación exporta HTTP como un servicio que se asigna a un puerto y directamente atiende peticiones que llegan a dicho puerto.
Esto típicamente se implementa declarando una dependencia a una biblioteca de servidor web, tal como Tornado en Python, Thin en Ruby o Jetty en Java/JVM. Es así que todo es parte de la aplicación y solamente se expone el puerto correspondiente para atender peticiones.
Es posible exponer otros servicios además de HTTP por medio de asignación de puertos. Por ejemplo, una aplicación puede exponer un servicio de Redis o de XMPP, y a su vez esa aplicación puede operar como servicio externo (recurso conectable) para otra aplicación.
VIII. Concurrencia
Escalar usando procesos.
Cualquier programa computacional, una vez en ejecución, es representado por uno o más procesos. Sin embargo, tradicionalmente las aplicaciones web tienen muy poca visibilidad hacia los procesos que la ejecutan.
En una aplicación twelve-factor los procesos son ciudadanos de primer nivel. Una aplicación puede definir distintos tipos de proceso para manejar distintos tipos de petición Por ejemplo, las peticiones HTTP pueden manejarse por un proceso “web” mientras que las tareas de larga duración pueden ser manejadas por otro proceso “worker” que tiene diferente prioridad. Una aplicación puede tener varias instancias de un mismo tipo de proceso ejecutándose en el mismo nodo (escalar verticalmente) o puede agregar más nodos de cómputo y ejecutar los procesos en ellos (escalar horizontalmente).
Es importante que los procesos de la aplicación se puedan administrar por medio de los mecanismos provistos por el sistema operativo para administrar procesos (por ejemplo Upstart en Ubuntu).
IX. Desechabilidad
Hacer el sistema más robusto por medio de arranque rápido y apagado gentil.
Los procesos de la aplicación son desechables, es decir que pueden ser iniciados y terminados en cualquier momento. Esto facilita la escalabilidad y el despliegue de cambios en el código o la configuración.
“Apagado gentil” es la traducción más cercana que se nos ocurre de graceful shutdown. En este caso se refiere a que cuando un proceso recibe una señal de finalización (SIGTERM) por parte del administrador de procesos debe: 1) dejar de escuchar el puerto de servicio, 2) finalizar las peticiones pendientes (o regresarlas a una cola de trabajo) y 3) terminar su ejecución.
X. Paridad entre ambientes
Mantener los ambientes de desarrollo, preproducción y producción lo más similares posible.
Tradicionalmente ha habido una brecha entre los ambientes de desarrollo y producción que se manifiesta en tres áreas:
Brecha de tiempo: dado que las liberaciones a producción no son frecuentes, un desarrollador puede estar trabajando en código que tardará semanas o meses en estar en producción.
Brecha de personal: el equipo de personas que construye la aplicación es distinto al que la pone en producción.
Brecha de herramientas: en el ambiente de desarrollo se utiliza software distinto (herramientas, servidores, sistema operativo) que en producción.
En contraste, una aplicación twelve-factor está diseñada para entrega continua y para lograrlo reduce la brecha entre desarrollo y producción. Teniendo en cuenta las tres brechas descritas anteriormente, las acciones a tomar son:
Reducir la brecha de tiempo entre liberaciones, haciendo varias liberaciones a la semana o incluso al día.
Reducir la brecha en personal involucrando a los desarrolladores en el despliegue y monitoreo de la aplicación en producción.
Reducir la brecha en herramientas manteniendo los ambientes de desarrollo y producción lo más parecidos posible.
XI. Bitácoras
Trata las bitácoras como corrientes de eventos.
Las bitácoras (logs) nos permiten ver cómo se está comportando una aplicación en ejecución. Típicamente, éstas se escriben en archivos en disco (logfile), pero es tan solo un formato de salida.
Una aplicación twelve-factor no se preocupa por rutear o almacenar su bitácora ni por gestionar los archivos de bitácora. En lugar de ello, cada proceso envía su corriente de eventos directamente a stdout. Es así que en desarrollo, el programador típicamente verá la corriente al ejecutar la aplicación desde la terminal de comandos. En ambientes de producción y preproducción, el de ejecución captura y organiza la corriente de eventos de cada proceso, y la rutea a uno o más destinos para que se archive. Estos destinos no son visibles ni configurables por la aplicación, son completamente administrados por el ambiente de ejecución.
Esto facilita que las bitácoras puedan ser indexadas y analizadas por medio de herramientas de propósito específico como Splunk, o incluso por sistemas más genéricos como Hadoop/Hive.
XII. Procesos administrativos
Ejecutar las tareas administrativas como procesos de la aplicación.
Es común tener que realizar tareas administrativas o de mantenimiento en una aplicación, tales como ejecutar scripts para limpiar datos, exportar datos, o disparar una consola para ejecutar algún código arbitrario.
Este tipo de procesos administrativos se deberían manejar en un ambiente idéntico al de los procesos normales de la aplicación. Es decir, son parte del código base de la aplicación y se ejecutan en el contexto de una aplicación liberada, con la misma configuración. También se debe utilizar la misma técnica de aislamiento de dependencias (ej. bundle exec o Virtualenv) que se haya usado con los procesos normales de la aplicación.
Conclusión
Como arquitectos y desarrolladores debemos entender la diferencia entre una aplicación que puede funcionar en la nube y otra que está diseñada para aprovechar la nube. Los lineamientos descritos aquí brindan una guía de cómo lograr lo último.
Si quieres conocer más al respecto, te invito a revisar el manifiesto completo [1].
Referencias:
“The Twelve-Factor App”. https://12factor.net
Pedro Galván Kondo es cofundador y director editorial de Software Guru.
- Log in to post comments