Archivo

Archive for septiembre 2019

The Twelve-Factor App

domingo, 22 septiembre 2019, 1:13 Deja un comentario

The Twelve-Factor App es una metodología (según su web, aunque la veo más como un conjunto de best practices) aplicable al desarrollo de aplicaciones y servicios Web basados en el modelo de software as a service (SaaS).

Los motivos por los que nos puede interesar conocer y aplicar una guía de buenas prácticas como esta son muchos y variados:

  • Para tomarlo como un punto de partida sobre el que ir evolucionando en los procedimientos o técnicas de un equipo u organización
  • Por conocer cómo hacen otros compañeros de profesión las cosas y aprender de su experiencia
  • Por conocer las tendencias (que en estas cosas hay mucho valor pero también mucha moda)
  • Por establecer un punto común de partida en el equipo consensuado por todos del que luego ir evolucionando
  • Por ver diferentes opciones y tener juicio crítico para evaluarlas

En todo caso, la adopción, si conviene, debemos hacerla de una manera práctica y con juicio crítico. No es necesario implementarla al completo, solo aquellas partes que nos interesen o hacerlo de manera progresiva. También puede servir como un punto de partida del que luego iterar según nos dicte el contexto o nuestras necesidades a algo más apropiado para nosotros, que puede ser más o menos cercano a lo que nos cuenta la metodología. Y teniendo total libertad para decidir qué partes se adoptan y cuáles no o cuáles se hacen de una manera diferente, según lo que nos dicte el sentido común y el buen criterio. En todo caso no soy partidario de aplicarlas de manera dogmática, estricta y completa; porque eso puede convertir la herramienta en un lastre, lo que termina desacreditándola y haciéndole perder gran parte de su valor.

La metodología Twelve-Factor App está orientada a aplicaciones Web. No por su naturaleza Web en sí, sino por el modelo de negocio o explotación al que está orientado: software-as-a-service (SaaS). La gran peculiaridad de este modelo, frente a otros de software, es que solo se explota una instancia y una versión de la aplicación (servicio realmente) por lo que:

  • no existe fragmentación
  • su operación es única, sencilla y centralizada
  • su rollout es más sencillo y rápido

A su vez, por su propia naturaleza Web, son soluciones más escalables, accesibles, más independientes de plataforma y de menor coste.

La metodología está compuesta por 12 capítulos o factores. En algunos de ellos se nota esta orientación a SaaS, ya que serían difícilmente aplicables a otros modelos. Esto es relevante porque, según nos alejemos del modelo SaaS, su idoneidad es más cuestionable y podrían plantearse alternativas mejores.

TLDR ;-P

  • I. Base de código: Existe un solo repositorio de código y muchos despliegues o instancias
  • II. Dependencias: Las dependencias se declaran explícitamente y se gestionan aisladamente
  • III. Parametrización: La parametrización de cada entorno se gestiona independientemente y de manera aislada al código
  • IV. Backing services: Los backing services son recursos asociados a la aplicación pero desacoplados de su ciclo de vida
  • V. Construye, lanza y ejecuta: Las fases de construcción, lanzamiento y ejecución están aisladas y diferenciadas entre sí. La release lo conforman el código y la parametrización conjuntos
  • VI. Procesos o instancias concurrentes: La aplicación la conforman varios procesos concurrentes sin estado (shared-nothing architecture). El estado es un recurso compartido por todos los procesos
  • VII. Vinculación al puerto: Los servicios se publicitan mediante puertos. El descubrimiento de servicios se hace vía puerto, a nivel de red
  • VIII. Concurrencia: La aplicación escala fácil, sencilla y rápidamente a nivel de proceso
  • IX. Desechabilidad: Maximiza la resiliencia facilitando la creación y destrucción de procesos, haciéndolos contingentes y desechables
  • X. Igualdad de los entornos de desarrollo y producción: Los entornos de desarrollo y producción deben ser lo más parecidos posible
  • XI. Trazas: Trata las trazas de aplicación como flujos de datos. Homogeneiza su recolección y tratamiento
  • XII. Procesos de administración y operativa: Los procesos, tareas y componentes complementarios al core de la aplicación también deben seguir estas prácticas al ser parte del juego de procesos

I. Base de código

La relación entre código y aplicación es uno a uno, única. Sólo existe un repositorio de código para toda la aplicación.

Aún pudiendo existir diferentes instancias o despliegues de la misma aplicación, esta proviene siempre de un único repositorio de código (o varios solo si estos pueden vincularse a mismo commit o transacción).

Las instancias pueden ser de entre sí versiones distintas del mismo código y repositorio: producción, validación, integración, local del desarrollador…

A su vez, si el mismo código es utilizado en más de una aplicación, debe tratarse como una librería, como una dependencia, y no como una aplicación. Es la aplicación donde se integra dicha librería de código compartido a la que se le aplica esta metodología.

II. Dependencias

Las dependencias se declaran de manera completa (todas y cada una), unívoca (con su versión específica) y explícita (existe un mecanismo que las enumera y gestiona).

Todas las dependencias se empaquetan o despliegan con la propia aplicación de manera aislada al resto de los componentes que pueden existir con anterioridad en el mismo sistema (como se hace combinando virtualenv y pip en Python). La aplicación no debe depender de ninguna librería del sistema y entorno, si no es del entorno exclusivo de ejecución de la aplicación (por ejemplo, no se debe enlazar a una dependencia global de npm o de una utilidad existente en el sistema operativo).

Sin embargo, existen excepciones como drivers, software base o sistema operativo o la misma plataforma o framework de ejecución que son requisitos preliminares y que no pueden gestionarse de manera aislada. Esto no quita que deban enumerarse e identificarse entre las dependencias.

III. Parametrización

La parametrización es todo aquel valor, parámetro o variable que depende únicamente del entorno o instancia donde se ejecuta la aplicación. Las variables propias de la aplicación que no dependen del entorno o contexto (por ejemplo, la definición del contexto IoC de Spring) no se considera parametrización.

La parametrización nunca va incluida en el código y está completamente separada de este. Como prueba de esta condición se propone que evaluemos si podríamos ofrecer el código de manera abierta sin que se detallase ningún detalle que distinga los entornos o instancias.

La metodología recomiendo el uso de variables de entorno por ser agnóstico del entorno y lenguaje de programación. Yo particularmente, no estoy de acuerdo, puesto que algunas parametrizaciones pueden ser muy tediosas de configurar así, y porque formatos como JSON, XML o YAML son ampliamente soportados.

Las parametrizaciones de los entornos deben ser «ortogonales» o independientes entre sí. No deben estar agrupadas o asociadas (por ejemplo, en el mismo fichero), deben estar aisladas entre sí.

IV. Backing services

No he encontrado una traducción adecuada para backing service. Quizás servicio de apoyo o auxiliar, pero como tampoco me ha gustado he decido utilizar el término sin traducir.

La metodología denomina backing service a todo componente accesible mediante red. He añadido alguna matización en las notas al final del artículo. Por simplificar, yo considero backing service todo componente que no es parte de la release, con ciclo de vida independiente al de nuestra aplicación y con una versión o interface por general estático, que no cambia.

Ejemplos de backend services pueden ser una base de datos MySQL, una cola Kafka, el API de Google Maps o Facebook o el servicio de trazas AWS Cloudwatch.

La principal diferencia con las dependencias es que no forman parte de proceso de release por un lado, y que pueden intercambiarse por distintas instancias o incluso versiones durante el despliegue (generalmente mediante un parámetro) o en tiempo de ejecución, sin que por eso afecte a la funcionalidad o estabilidad de la aplicación.

Los backing services deben ser totalmente desacoplados y aislados de la aplicación. Son intercambiables. Se comportan como recursos enlazados, anexos o adjuntos a la aplicación mediante parametrización generalmente. Tienen un ciclo de vida completamente independiente al de la aplicación. Todo lo contrario que las dependencias que eran parte del build y release.

En las arquitecturas basadas en recursos efímeros como puede ser la basada en containers. Los componentes no efímeros, los servicios de persistencia o almacenamiento por ejemplo, pueden considerarse backing services, porque tienen un ciclo de vida alternativo al del resto de los componentes y pueden sustituirse o desacoplarse de manera independiente.

V. Construye, lanza y ejecuta

El objetivo de la base de código es el despliegue en un entorno productivo.

Este proceso se compone de 3 fases diferenciadas: construcción, lanzamiento y ejecución de la instancia o entorno.

Construcción

Construcción es el proceso en el que se toma el código de un punto específico o versión, se recolectan las dependencias y se generan los binarios y recursos que forman el producto o build.

La construcción de la misma versión de código siempre producirá el mismo build. Las dependencias, la configuración y versiones de las herramientas utilizadas durante la construcción; así como los recursos no compilados son parte de la propia versión, de la base de código. La construcción al ser un proceso determinista siempre tendrá el mismo resultado.

Lanzamiento

El lanzamiento es la combinación del build del proceso de construcción junto con la configuración propia del entorno o instancia, para crear el lanzamiento o release.

La configuración del entorno es la parametrización propia del entorno, aquellos grados de libertad que el código permite configurar en la instancia.

La configuración no forma parte de la base de código ni del build, pero sí de la release.

La release está vinculada entonces a un momento puntual en el tiempo del código y con una parametrización del entorno en particular. Es el estado momentáneo de la instancia o servicio. La release es siempre etiquetada siguiendo una nomenclatura descriptiva de ambos componentes, la versión del software y de la parametrización del entorno.

Que parametrización y código se congelen en un punto común, como es la release, tiene sentido puesto que el código puede comportarse entre versiones de manera distinta para un mismo parámetro o porque una parametrización distinta cambia el comportamiento del mismo código. Código y configuración son dos grados de libertad que quedan vinculados en la release.

La release es el estado concreto que se ejecuta en una instancia y en un momento dados. Si queremos replicar el estado software de una instancia, para replicar un bug en un entorno alternativo por ejemplo, tendremos que desplegar esa release en el entorno de desarrollo del bugfix.

Ejecución

Lo que se despliega en la instancia o múltiples instancias del entorno es siempre la release. Tanto si el proceso es manual o como si es automatizado o si existen mecanismos de autoescalado en múltiples instancias, en todas ellas se va a desplegar y ejecutar siempre la misma release.

Mientras que la generación de build y release son procesos originados solo por el cambio del código, la ejecución de la release se produce en muchas ocasiones: en cada escalado, en cada reinicio, en cada proceso batch, de manera constante en arquitecturas efímeras…

Otra gran ventaja de este modelo es que los rollback de los despliegues son mucho más directos y sencillos al contener la release tanto build como configuración.

Existen casos en los que en un mismo entorno pueden haber diferentes releases desplegadas y ejecutando a la vez, como pueden ser los despliegues de pruebas A/B o los procedimientos de despliegue Blue/Green.

VI. Procesos o instancias concurrentes

La aplicación se ejecuta como un conjunto de procesos aislados sin estado y con una arquitectura o filosofía shared-nothing.

Si la aplicación debe persistir su estado o transacciones lo hace en un backing service dedicado como puede ser una base de datos. El estado solo se persiste ahí. Esto permite gestionar instancias de manera independiente con el estado y aporta mucha mayor resiliencia.

La arquitectura shared-nothing se basa en el hecho de que cualquier transacción debería poder ser servida por cualquier nodo o parte replicada de la aplicación, por cualquier proceso. Todos los procesos tienen acceso a la misma información mediante el backing service que hace de capa de persistencia. Puesto que toda la información es compartida y modificable por los procesos, no se recomienda el uso de cachés que pueden ocultar cambios de estado posteriores o que pueden requerir complejos refrescos de la información (patrón de saga).

Mecanismos del tipo sticky session que vinculan la sesión del cliente o usuario con unos procesos determinados no son recomendables tampoco por estos criterios de aislamiento o de procesos sin estado. La información de sesión es preferible que se almacene en una caché de memoria compartida o similar (memcache o Redis por ejemplo).

VII. Vinculación al puerto

Las aplicaciones Web suelen correr sobre una plataforma o contenedor Web que abstrae todo el networking y lógica de los protocolos de red. En esencia, el interfaz último es el puerto de red del servicio. En los ejemplos que menciono es HTTP, pero puede ser cualquier otro protocolo.

Se podría considerar que servidores Web como Tomcat o un módulo de Apache son una base independiente necesaria, como si de un backing service se tratase. La metodología sugiere lo contrario, que se traten como dependencias propias de la aplicación, que se despliegan con la propia aplicación y forman parte de la release. Se sugiere a modo de ejemplo embeber servidores como Jetty, Thin, Tornado, en lugar de las opciones independientes.

El fin es que el interfaz último sea este puerto/servicio de red, que sea ese el punto de acceso y referencia a la aplicación. La presentación pública del servicio no es más que un enrutamiento a ese punto de acceso, sea a nivel de red (mediante DNS) o a nivel de protocolo (mapeado de URLs).

Por otro lado, lo que se persigue también es que la unidad de despliegue sea una caja negra hasta ese nivel de red, que dicha aplicación sea autocontenida e independiente. Con las siguientes ventajas

  • Los entornos de desarrollos y productivos son mucho más parecidos al conservar los mismos servidores y configuraciones
  • Los modelos de concurrencia u orientados a procesos, como se describen en los apartados VI y VIII, se simplifican
  • Cualquier aplicación puede ser un backing service de manera inmediata, siendo referenciada a su puerto de servicio, para otra aplicación
  • Puesto que el interfaz es el servicio de red, la sustitución de estos servicios Web embebidos es transparente, como si de una dependencia se tratase
  • Al ser una unidad autocontenida, los requisitos necesarios para el entorno que vaya a correr los procesos son mucho menos restrictivos y la aplicación puede desplegarse en una variedad mayor de entornos

VIII. Concurrencia

Para facilitar la concurrencia y el escalado de la aplicación, se considera el proceso como la unidad básica de la aplicación.

El modelo recomendado es el de los procesos UNIX que corren demonizados. La gestión del proceso no se vincula a ningún mecanismo en particular, sino que se deja en mano de los mecanismos más generales y comunes del sistema donde corre la aplicación (como systemd).

El diseño de la aplicación implica considerar qué tipos de procesos diferentes existen para cada diferente necesidad (web, batch, stream de eventos…). La naturaleza de cada proceso se adapta a la necesidad que cubre. La instancia de la aplicación tendrá entonces un número de procesos concurrentes corriendo para cada tipo de tipo en particular. El conjunto de tipos de procesos y su número es lo que denomina el juego de procesos.

La operación de la plataforma conlleva la concurrencia y replicación de esos tipos de procesos para cubrir la demanda o carga de cada uno. Este modelo permite que el escalado sea muy sencillo y favorece la filosofía share-nothing. Añadir un nuevo tipo es muy sencillo y podría hacerse sin afectar al resto. Escalar un tipo añadiendo más procesos concurrentes, que pueden estar en la misma máquina física o no, también es sencillo e independiente a los existentes.

IX. Desechabilidad

Los procesos deben ser fácil y rápidos de lanzar y de terminar, y el impacto en la operación o en el servicio de estos cambios debe ser mínimo. El proceso es desechable. El objetivo es que las partes que componen el servicio sean fácilmente y rápidamente invocables y sustituibles.

De esta manera, el escalado del servicio replicando los procesos es muy sencillo. Si la replicación de estos procesos es ágil y sencilla, el escalado también lo es.

De igual manera aplicar cambios en la configuración, o incluso hacer la marcha atrás de estos cambios en caso de error, es también muy rápido y sencillo; tan solo hay que sustituir unos procesos por otros con la nueva configuración. Si la facilidad y rapidez con que se terminan los procesos y se comienzan los nuevos es alta, el impacto y el riesgo en el servicio de los cambios será menor.

Los procesos deben de terminarse de una manera sencilla, elegante y estable. Desde el momento que recibe la señal de finalización, el servicio o recepción de peticiones debe pararse y la petición en curso debe finalizarse completa y correctamente. Si hablamos de procesos encolados, la petición en curso debe devolverse a la cola, por ejemplo.

Dada esta naturaleza desechable, el impacto de una terminación no controlada o no limpia de un proceso debe ser también controlado o al menos mitigado y acotado.

El resultado de los procesos también debe ser desechable, bien porque forma parte de algún tipo de transacción (de tal manera que pueda deshacerse) o bien porque es idempotente, para que su reinvocación no modifique erróneamente el resultado.

X. Igualdad de los entornos de desarrollo y producción

Toda la metodología está orientada a un entorno de despliegue/integración continuos en los que el gap tanto temporal como técnico entre los entornos es mínimo:

  • el tiempo entre que una característica está disponible para producción (definición de done done) y su despliegue es muy pequeño, de horas incluso
  • las personas involucradas en ambos entornos son las mismas (DevOps) o al menos, no existe desentendimiento por parte de unos en el entorno de otros
  • los componentes, backend services y dependencias son los mismos en todos los entornos

La metodología hace mucho hincapié en no sustituir componentes por otros sucedáneos o lightweight, sino usar los mismos elementos software. Hoy en día esto es relativamente sencillo mediante las tecnologías de container o de cloud, que permiten desplegar el mismo componente pero con dimensionamientos apropiados para cada entorno.

Los entornos pueden ser efímeros, de tal manera que sean creados desde cero por completo cada vez que se despliegan, que no se actualicen conservando elementos de la versión anterior. Esto evita que haya deriva en los entornos (que tengan cambios no documentados, que se degraden con el tiempo…) y fuerza que los entornos siempre sean iguales en producción y desarrollo. Por supuesto, los componentes o backend services responsables de la persistencia de datos o del balanceo de carga no pueden ser efímeros, ya que impactaría en la integridad de los datos y la disponibilidad del servicio respectivamente.

XI. Trazas

Todos y cada uno de los procesos de la aplicación deben producir logs o trazas que reflejen su estado y procesamiento. Estas trazas deben interpretarse como flujos o streams de eventos ordenados.

Habitualmente esos flujos son gestionados por cada proceso o componente de manera independiente, enviándolos a la consola de salida, a un fichero o un colector syslog, por ejemplo. La metodología propone que los flujos de logs de manera general en todos los procesos se manden a la consola de salida estándar y sea el propio entorno el que recoja estos flujos y los gestione adecuadamente.

Es el entorno o el proceso de despliegue del entorno el que decide cómo recolectar y gestionar todos los flujos de logs para cada uno de los componentes.

Estos flujos pueden agregarse en colectores de logs, como puede ser Devo, que permitan:

  • Almacenar y agrupar los logs de todas las fuentes durante el tiempo necesario
  • Analizar eventos y transacciones y su flujo o procesamiento a través en los componentes
  • Correlar eventos y condiciones para detectar anomalías o situaciones específicas
  • Agregar la información para mostrar gráficos, dashboards o vistas agregadas

XII. Procesos de administración y operativa

Además de los procesos online que ofrecen el servicio core de la aplicación existen otros procesos que corren en el entorno y que son también fundamentales:

  • Procesos batch periódicos corriendo en background
  • Operaciones o procedimientos de gestión específicos
  • Tareas de procesamiento propias del despliegue como son migraciones o verificaciones de integridad
  • Procesos de copia de respaldo o de soporte

Todos y cada uno de estos procesos complementarios:

  • corren en el mismo entorno
  • pertenecen a la misma base de código o versión
  • se prueban y validan en el mismo proceso y a la vez que el resto del código
  • forman parte de la misma configuración, release y proceso de generación de la aplicación
  • forman parte del mismo proceso de despliegue

Todas las consideraciones tenidas en cuenta en las secciones anteriores deben también hacerse para estos procesos complementarios, como son la concurrencia, aislamiento de procesos, dependencias, tolerancia al error, traza…

Notas personales

Dependencias vs backing services

La metodología distingue dos términos, dependencias y backing services, y hace un tratamiento y recomendaciones muy distinto de ellos.

En la metodología considera backing service todo aquel servicio que es accesible mediante conectividad de red, pero no tiene por qué ser necesariamente así: un servicio local de disco (como puede ser EBS de AWS) es accesible de manera local, no de red (al menos desde el punto de vista de la aplicación) y en mi opinión es claramente un backing service y no una dependencia.

Por otro lado, las dependencias son consideradas librerías generalmente. Sin embargo, es posible encontrarse librerías o plataformas, que son más propias del entorno o contexto de ejecución o despliegue que de la propia aplicación. Por ejemplo, las librerías de ejecución de Node o Python disponibles en los contextos de ejecución de las Lambda de AWS o Function de Azure, son dependencias del código. Pero el modo en que vamos a poder gestionarlas es más un backing service que una dependencias, y aún así es discutible, porque no podemos cambiar una por otra de manera sencilla.

Puesto que las recomendaciones para una y otra son tan distintas, creo importante añadir otros criterios para distinguirlas. Estos son de cosecha propia:

  • Todo servicio, generalmente otro SaaS, que tenga un ciclo de vida diferenciado del de nuestra aplicación, sea propio o de un tercero, es claramente un backing service.
  • Las librerías del sistema o de plataforma, propias del contexto de ejecución o despliegue de la aplicación, pueden considerarse también un backing service.
  • Los elementos que vienen más impuestos por el contexto de ejecución o despliegue que por las propias funcionalidades de la aplicación, suelen ser backing services.
  • El código compilado durante el proceso de build y empaquetado durante el proceso de release, es dependencia.
  • Los elementos que se integran o asocian a la aplicación mediante parametrización, es muy posible que sean backing services.
  • Las dependencias integradas en tiempo de ejecución, como puede ser con Spring a través de IoC, pueden considerarse también backing service. La dependencia como tal, es el interfaz compilado. La librería cargada y enlazada en tiempo de ejecución podría ser un backing service.

Los backing service también tienen versionados y evoluciones con diversos criterios de compatibilidad. Tiene sentido que estas versiones sean menos dinámicas que las de las dependencias, pero no por ello pueden ignorarse. Por ejemplo, los servicios en la nube, como Google Maps, evolucionan y modifican sus interfaces. Es por ello también importante etiquetar o gestionar estas versiones.

Releases por entorno vs releases por versión del código

Uno de los aspectos que más sorprende de esta metodología es que la release contenga siempre la configuración del entorno donde se pretenda desplegar y que el despliegue sea un todo, no la instalación de un software en un primer paso y su parametrización posterior.

La principal ventaja de esta consideración es que el componente probado en los sucesivos entornos de validación, así como en las pruebas de integración de los mismo, es mucho más parecido al entorno final al contener la parametrización. Además el conjunto de lo desplegado es más compacto, unitario y tiene menos posibles puntos de error, al ser todo uno.

Ahora bien, cuando el conjunto de instancias o entornos a mantener es limitado esto es factible. Pero cuando tenemos un conjunto de instancias muy grande o cuando, sencillamente, no están bajo nuestro control, porque son despliegues en manos de terceros por ejemplo; esto es complicado.

Esta consideración es así porque la metodología es aplicable al modelo de SaS y no al de producto o software en propiedad. En estos casos cada instancia está siempre bajo el ciclo de vida del proveedor de extremo a extremo y el conjunto de instancias que dan el servicio es muy controlado y las releases gestionadas reducido (si bien en número de instancias puede ser enorme, en configuración son clones entre sí y comparten todas la misma release).

En la modalidad de software en propiedad, la parametrización es gestionada por el propietario de la instancia que no suele ser el proveedor de software. Por este motivo la release contiene solo el build y no incluye parametrización ninguna.

Categorías: Arquitectura, Desarrollo
A %d blogueros les gusta esto: