featured image

De vez en cuando la gente se reúne y medita sobre las acciones que se están llevando a cabo para el desarrollo de software. Esta actividad de reflexión es muy positiva y nos proporciona estudios o manifiestos muy interesantes. En este caso quiero hablar de uno llamado The Twelve-Factor App para aplicaciones web que se ofrecen como servicio (Software as a Service, SaaS), ¿de qué se trata?

Este documento (en castellano y otros idiomas), ofrece 12 puntos (o factores) para conseguir un software que cumpla:

  • El uso de formatos declarativos para la automatización de la configuración.
  • Máxima portabilidad a través de un contrato claro con el sistema operativo.
  • Despliegue en plataformas en la nube y no depender de una máquina en concreto.
  • Despliegue continuo minimizando las diferencias entre desarrollo y producción.
  • Escalar sin que suponga cambios significativos en las herramientas, arquitectura o prácticas.

Se puede aplicar a cualquier lenguaje de programación y a cualquier combinación de base de datos, colas, memorias cache, etc.

Dando un repaso rápido a cada uno de los factores tenemos:

1. Código base único

Un solo código base del que realizar los despliegues, sin cambios. El código base omite detalles como los ficheros de configuración que serán las únicas diferencias que deben existir entre los distintos entornos.

En este sentido se recomienda mantener repositorios de control de versiones con el código base y siempre realizar despliegues construyendo desde la rama principal. Los artefactos creados y probados son los que se pondrán en producción.

Para poder poner un artefacto en producción la configuración diferente de desarrollo a producción debe omitirse y no incluirse dentro del artefacto. El entregable debe tener únicamente binarios y la configuración debe provenir de otra fuente ya sea dinámica como etcd o cualquier otro.

2. Dependencias aisladas del sistema

Declarar y aislar las dependencias. Intentar mantener todas las dependencias instaladas dentro del proyecto al realizar el despliegue. Para esto se pueden usar herramientas de vendoring en muchos de los entornos de desarrollo.

Sistemas como Java (Maven), Erlang (rebar, erlang.mk y rebar3), Elixir (mix a través de hex) implementan la instalación de estas dependencias en el artefacto a ser desplegado mientras que otros como Rails o Django requieren de otras artimañas al contener sus dependencias como gemas y eggs respectivamente y debiendo instalarse junto con el intérprete en lugar del proyecto.

La idea base es no incluir estas dependencias en el repositorio de control de versiones del código del proyecto. En ese repositorio podemos agregar la configuración para construcción del proyecto y obviamente la toma de esas dependencias.

3. Configuraciones en el sistema

Guardar la configuración en el entorno. Como se menciona en el punto 1, las diferencias entre los entornos estarán principalmente en la configuración y mantener la configuración ligada al entorno es lo más fácil para realizar despliegues.

Como dije antes lo ideal es disponer de un sistema dinámico con la configuración pero si esta no es una opción se puede optar por emplear variables de entorno del sistema operativo para la configuración. De esta forma podemos adecuar el entorno de ejecución con estas variables y después ejecutar nuestro sistema.

4. Servicios de apoyo conectables

Tratar a los backing services (servicios de apoyo como bases de datos, colas, memorias caché, servicios de email, etc.) como servicios conectables. Al no estar ligados a la máquina donde se ejecuta el código base, facilita mover estos servicios a otras máquinas cambiando únicamente la configuración.

La idea es no crear dependencias dentro de la máquina donde se despliegue nuestra solución. Los elementos conectables y de información persistente o de apoyo como memorias caché o servicios de email deben mantenerse aparte. Esto también beneficia si en un momento dado necesitamos delegar el mantenimiento de la base de datos, o emplear un sistema de envío de emails no mantenido por nosotros.

5. Construir, distribuir, ejecutar aislados

Definimos tres etapas que deben estar completamente separadas:

  • La etapa de construcción es donde se genera la construcción trayendo todas las dependencias para generar el artefacto (o paquete que incluirá todo). En esta fase necesitaremos de herramientas que no tienen porqué estar disponibles en el entorno de ejecución. Herramientas como un compilador de lenguaje C u otras herramientas similares deben estar disponibles únicamente en un entorno de construcción.

  • La etapa de distribución se mezcla el artefacto con la configuración para realizar el despliegue. Normalmente se emplea un directorio diferente al usado para la distribución anterior de modo que se mantengan ambas. La subida del artefacto puede hacerse a un repositorio interno o a alguna solución de la nube.

  • La etapa de ejecución (conocida como runtime) se ejecuta la aplicación en el entorno. También es ideal que se pueda ejecutar (si es posible) en otro proceso diferente para mantener la versión anterior hasta que la nueva esté completamente operativa. Es habitual incluso poder realizar pruebas A/B con los usuarios dejando un sector de los usuarios accediendo al nuevo servidor y el resto accediendo a la versión anterior.

Lo ideal es emplear herramientas que nos permitan generar cada paso y proporcionen incluso mecanismos para detener el proceso de despliegue si algo va mal. Es usual emplear para estos despliegues herramientas de control y automatización de configuración como Ansible.

6. Procesos sin estado

Ejecutar la aplicación como uno o más procesos sin estado. La motivación es no mantener información que deba ser compartida en una sola máquina porque eso nos limita a esa única máquina. Emplear el sistema de ficheros por ejemplo, nos limita a esa máquina. En su lugar es siempre preferible emplear sistemas como GlusterFS, LeoFS o incluso otras soluciones en la nube como Amazon S3 para mantener los ficheros a modificar accesibles desde todas las instancias del servicio.

7. Asignación de puertos

Cada aplicación debe ser autocontenida y tener un puerto interno al que acceder diferente al del resto de aplicaciones. Esta asignación se realiza en la etapa de distribución y consiste en configurar la capa de enrutamiento que se encarga de llevar las peticiones de la IP pública a la IP interna y el puerto asignado.

Hay sistemas como Docker que realizan esta configuración a la hora de iniciar el contenedor y otras soluciones en la nube como AmazonWS o DigitalOcean que requieren de una configuración de un nombre de dominio que se asigne a varias direcciones IP para balancear la carga de ese servicio dado. De esta forma conseguimos versatilidad y flexibilidad en nuestras configuraciones pudiendo acceder siempre desde la misma IP y puerto al servicio esté donde esté.

8. Escalar mediante el modelo de procesos

Los procesos de las aplicaciones twelve-factor se inspiran en el modelo de procesos unix para ejecutar demonios. Lo ideal es escribir scripts de tipo SysV o systemd, dependiendo del sistema donde se despliegue la aplicación.

No obstante actualmente ya sea mediante el uso de supervisord o Docker se prefiere que la aplicación se mantenga en primer plano para ser supervisada o contenida, según el caso.

En base disponemos de tres opciones para tener nuestras aplicaciones siempre activas:

  • Ejecutar un demonio en el sistema para garantizar con un reinicio de la máquina la operatividad de nuestro sistema. El servidor o sistema debe mantenerse en segundo plano en este caso y hacer uso de los sistemas de scripts de SysV o más preferiblemente de systemd. A través de la ejecución de un sistema como Puppet o Ansible podemos garantizar la configuración correcta y funcionamiento del sistema además.
  • Ejecutar la aplicación en primer plano desde supervisord. Este sistema monitoriza en segundo plano una ejecución y en caso de finalizar establece unos parámetros para volver a iniciarla y/o notificar sobre su detención.
  • Ejecutar la aplicación en primer plano pero contenida, es decir en un contenedor de estilo Docker o rkt. Estos sistemas permiten monitorizar la aplicación y establecer reinicios además de controlar los recursos empleados por la ejecución del programa y establecer límites.

9. Disponibilidad

Hacer el sistema más robusto intentando conseguir inicios rápidos y finalizaciones seguras. Como continuación del punto anterior, consiste en poder iniciar o detener el sistema en cualquier momento y minimizar estos tiempos para asegurar la disponibilidad del sistema.

Obviamente detener el sistema afecta directamente a la disponibilidad pero existen técnicas para poder detenerlo paulatinamente mientras lo volvemos a iniciar. Es decir, si disponemos de 5 máquinas y necesitamos realizar un reinicio por una actualización o por cualquier problema/motivo podemos apagar en principio una sola máquina, realizar los cambios y volver a levantarla y repetir el mismo proceso en cada una de las máquinas consiguiendo un tiempo de caída poco significativo en caso de afectar a alguna conexión.

Esta técnica combinada con un balanceador de carga donde podamos eliminar la máquina del balanceo y drenar sus conexiones de forma segura antes de apagarla nos garantiza un mayor tiempo de disponibilidad para nuestros usuarios.

Sin embargo esta técnica no siempre es posible. Si una actualización requiere un cambio en base de datos o en un elemento no compatible con la versión anterior. En el momento de realizar el cambio de datos o del elemento en cuestión podríamos encontrar la desagradable sorpresa de tener inoperantes todos los antiguos servidores.

En este caso apagar todo el sistema de forma rápida y conseguir volver a levantarlo con la nueva versión de forma rápida es un requisito indispensable.

10. Igualdad entre desarrollo y producción

Mantener desarrollo, preproducción y producción tan parecidos como sea posible. Consiste en realizar despliegues de forma continua para garantizar la uniformidad del código en todos los entornos. La idea es intentar no diferir durante mucho tiempo cuando se realizan modificaciones en estos entornos.

En este caso nos puede ayudar la metodología de Integración Continua (CI por sus siglas en inglés) y Despliegue Continuo (CD por sus siglas en inglés).

La primera (CI) nos garantiza la integración de todos los cambios en un entorno operativo como puede ser un entorno de desarrollo. La idea es agregar cada cambio realizado en el código base para la rama principal siempre en el entorno de desarrollo a modo de siempre trabajar con los últimos cambios y realizar pruebas sobre el conjunto del sistema una vez se han aplicado los cambios. Como dice José Valim: Fail fast o fallar rápido para poder corregir y continuar.

La segunda (CD) nos garantiza un flujo de mejora en producción al introducir los cambios validados en el entorno de desarrollo hacia el entorno de producción. La idea es realizar estos despliegues de forma periódica y frecuente para minimizar el impacto de liberar gran cantidad de código con gran cantidad de cambios que pueda ser un problema por el simple hecho de su magnitud.

11. Logs como eventos

Es ideal no mantener los logs (o historiales) como ficheros locales, sino que sean enviados a sistemas centralizados donde puedan ser analizados por personas cuando suceda algún error o por sistemas automáticos que puedan generar alarmas o alertas para reaccionar antes ante los errores.

Para estas tareas antiguamente se delegaba en el protocolo syslog debido a su simpleza y rapidez (trabaja sobre UDP y los paquetes de datos son muy pequeños y en texto plano). A día de hoy se puede aún ver soluciones en la nube como Papertrail o Graylog que emplean este protocolo para el envío de los logs a sus sistemas.

Por otro lado podemos optar por la generación de logs enriquecidos en formato JSON y emplear plataformas como ELK (ElasticSearch, Logstash y Kibana) para el análisis de esta información de forma centralizada. La empresa Elastic ofrece servicios en la nube también basados en su solución.

Podemos encontrar muchas más alternativas ya sea en servicios Open Source, propietarios o como servicios en la nube.

12. Tareas de gestión no permanentes

Ejecutar las tareas de gestión/administración como procesos que solo se ejecutan una vez. Lo ideal es configurar a través de cron todas las tareas que necesiten realizarse de forma periódica y evitar lo máximo posible el uso de demonios para este tipo de tareas.

Agregar además la correcta configuración de cron para ser notificados en caso de fallo al realizar una tarea y agregar los logs para la ejecución de estas tareas al sistema de logs del punto 11 debería ser de obligado cumplimiento también.

Si tenemos un sitio web donde los clientes subscriben productos con nosotros una tarea no permanente sería el envío de notificaciones cuando el saldo del cliente es bajo, cuando haya que enviar facturas o cuando requiramos el pago periódico de un servicio.

También podemos emplear estas tareas para ejecutar un script consumidor encargado de leer todos los mensajes disponibles en una cola y procesarlos. Si la frecuencia de encontrar un mensaje en la cola no es muy alta este sistema sería ideal para no tener ocupada la memoria y el programador de tareas de la CPU con un consumidor permanente.

Conclusiones

Como dije al inicio estas son las reflexiones a las que llegaron un conjunto de personas que trabajan en diversos lenguajes, diversas plataformas y con distintas tecnologías de virtualización para despliegue de sus productos y proyectos. Además de mis anotaciones y pensamientos sobre cada una de ellas.

La mayoría de estos puntos los puedes encontrar más desarrollados en el sitio web 12factor.net. Desde mi punto de vista todos son muy coherentes y garantizan el despliegue fácil del código en cualquier plataforma y en cualquier tamaño elegido (número de servidores).

¿Sigues alguna de estas indicaciones? ¿no te convence alguna de ellas? ¿quieres saber más sobre cómo poder integrar esta metodología en tus sistemas? ¡Déjanos un comentario!