F5 es una empresa que provee un conjunto de productos relacionados a networking: firewall, balanceador, etc. Es común encontrarse con balanceador F5 en ambientes productivos de alta carga para repartir carga entre varios nodos.
Al mismo tiempo, en aplicaciones de cierta criticidad, es común que las actualizaciones se hagan siguiendo alguna estrategia tipo «canary», esto es:
«Se saca» un nodo del balanceador
Se le instala la nueva versión de la aplicación
Se verifica que la aplicación funcione correctamente
«Se restaura» el nodo en el balanceador
Se repite el proceso con cada uno de los restantes nodos
Cuando pretendemos trabajar en un esquema de continuous delivery es imprescindible que este proceso se haga de forma automatizada y para ello es necesario poder interactuar con balanceador programáticamente.
Dicho esto, he aquí un fragmento de código Python para interactuar un F5 Big-IP:
# este script depende de varias variables:
# * f5_login_url (url del balanceador para obtener el token)
# * f5_user (usuario de f5 para obtener el api token)
# * f5_pass (password de f5 para obtener el api token)
# * f5_self_service_url (url de la api del balanceador)
# * node_name (nombre del nodo sobre el que queremos operar)
# * node_id (id del nodo sobre el que queremos operar)
# * pool_id (id del pool del balanceador en el que está el nodo)
import requests
import json
import sys
# primero hay que loguearse y obtener un token
login_request = '{ "username":' + f5_user + ', "password":' + f5_pass }'
response = requests.post(f5_login_url, login_request)
if response.status_code == 200:
response_json = json.loads(response.text)
token = response_json["token"]["token"]
else:
token = {}
print('No pudo obtener token', file=sys.stderr)
exit(1)
# sacamos el nodo
headers = { 'X-F5-Auth-Token': token }
disable_request = '{"name":"Self-Service_' + node_name + '", "resourceReference":{' \
+ '"link":"https://localhost/mgmt/cm/adc-core/working-config/ltm/pool/' + pool_id \
+ '/members/' + node_id + '"},"operation":"force-offline"}'
response = requests.post(f5_self_service_url, data=disable_request, headers=headers)
# restauramos el nodo
enable_request = '{"name":"Self-Service_' + node_name + '", "resourceReference":{' \
+ '"link":"https://localhost/mgmt/cm/adc-core/working-config/ltm/pool/' + pool_id \
+ '/members/' + node_id + '"},"operation":"enable"}'
response = requests.post(f5_self_service_url, data=disable_request, headers=headers)
En mi opinión gran parte de la popularidad que alcanzó Heroku se debió a la facilidad con la que era posible desplegar una aplicación: git push. Básicamente teniendo el código fuente en un repositorio Git, solo basta con agregar un nuevo remote provisto por Heroku y hacer push al mismo. Cuando Heroku recibe el código fuente ejecuta un build-pack que básicamente «preparar el ambiente» para poder ejecutar el código recibido. Dependiendo del lenguaje ese build-pack puede instalar dependencias, compilar e incluso ejecutar migrations. Esta es seguramente la estrategia más utilizada al usar Heroku pero no es la única. Otra opción es ejecutar directamente un contenedor Docker. Esta opción trae un poco más de complejidad pero al mismo tiempo trae algunos beneficios interesantes.
La opción de utilizar un contenedor nos da más «libertad»/flexbilidad. Con el modelo de ejecución tradicional nuestra aplicación está restringida a lo establecido por el build-pack (aún cuando es posible crear build-packs, eso ya tiene otro costo), mientras que al correr un contenedor podemos poner lo que querramos dentro. Al mismo tiempo, al poder especificar el Dockerfile tenemos la posibilidad de ajustar ciertos aspectos del runtime. Finalmente, el correr un contenedor nos también la libertad de salir de Heroku a un costo más bajo.
En el contexto de MeMo2 @ Fiuba, utilizamos Heroku en modo «tradicional» durante el primer trabajo grupal pero para el segundo TP grupal tenemos la intención de utilizar un modelo basado en contenedores ya que queremos estudiar algunas de las implicancias de este modelo en términos de configuración management y deployment pipelines.
Explicada la motivación veamos entonces algunas cuestiones de implementación. En primer lugar debemos crear la imagen del contenedor que querramos ejecutar, para esto podemos delegar la creación de la imagen en Heroku o bien podemos crearla nosotros mismo y luego darsela a Heroku. Una vez que la imagen está construida y subida Heroku, basta una invocación a API rest para desplegarla (también es posible haciendo uso del heroku-cli). Al mismo tiempo, si es necesario ejecutar algún tipo de inicialización antes de levantar el contenedor Heroku nos da posibilidad de ejecutar un esa inicialización con otra imagen. Esto eso, debemos crear una imagen llamada «release», subirla a Heroku y cada vez que disparemos un deploy Heroku, se ejecutar primero la imagen release para hacer las tareas de incialización y luego se pondrá a correr el contenedor de nuestra aplicación. Todo esto está explicado con bastante detalle la documentación oficial de Heroku.
Llevando todo esto a nuestro escenario de MeMo2, vamos construir nuestra imagen Docker como parte nuestro pipeline de GitLab y vamos a almacenarla en la propia registry de Gitlab. Luego en el paso de deploy haremos un push de la imagen a Heroku y dispararemos el deploy via la API Rest. A continuación comparto algunos snippets de código de nuestro pipeline que puede resultar útiles para quienes pretendan implementar una solución similar con Heroku y Gitlab.
El siguiente fragmento de código corresponde a job de GitLab que crea la imagen Docker y la publica en la registry de GitLab
A continuación tenemos el fragmento de código correspondiente al job de deploy el cual descarga la imagen a desplegar de la registry de Gitlab y la sube Heroku. Finalmente invoca a script de deploy que interactua con la API Rest de Heroku.
Estos fragmentos de código requieren del uso de un API token de Heroku que debe ser configurado en las variables del ambiente del pipeline.
Ambos jobs del Gitlab asumen la existencia del un archivo VERSION.txt que contiene el tag correspondiente a la imagen docker que construye/publica/despliega. Típicamente ese archivo se genera en el build y se lo propaga por el pipeline o bien está en el repositorio de código fuente.
Estos fragmentos son ejemplos que pueden ser mejorados y/o ajustados para contextos más específicos. De hecho es muy posible que en los próximos días les aplique algunos ajustes.
Se conoce como Feature Toggles (o feature flags) a la capacidad/funcionalidad de disponibilizar (prender/apagar) una funcionalidad en base a un determinado criterio. En mi proyecto veníamos usando esta técnica casi desde que empezamos y en particular la usamos para habilitar funcionalidades gradualmente a distintos grupos de usuarios. De entrada hicimos nuestra propia implementación de feature toggles, la cual ofrecía ciertas capacidades acotadas pero que para nuestro escenario resultaban suficientes.
Nuestro Product Owner se entusiamó con esta capacidad de «togglear» funcionalidades y entonces decidimos analizar una solución de toggles más robusta y flexible. Es así que comenzamos a analizar las opciones listadas en el sitio FeatureFlags.io. Por otro lado nos encontramos con el componente Microsoft.FeatureManagement. A simple vista este componente nos resultó el más atractivo de los que habíamos visto y por ello decidimos probarlo.
El resultado fue contundente, nos llevó aproximadamente 1 hora reemplazar nuestro componente casero por el componente de Microsoft y lo que pensábamos que sería una prueba de concepto terminó completamente integrado en nuestra aplicación.
En forma resumida el uso de este componente requiere de los siguientes pasos.
1. Agregar el paquete
Agregar la referencia al paquete Microsoft.FeatureManagement.AspNetCore
2. Agregar la configuración
La configuración se puede poner en un archivo json o directamente se puede incluir en el archivo de configuración de la aplicación (appsettings.json).
Este fragmento de configuración indica que la «funcionalidad1» está encendida, que la funcionalidad2 está apagada y que la funcionalidad3 está encendida solo para los usuarios juan y maria.
3. Agregar los toggle points
Los toggle points son los lugares en nuestro código donde se consultan los toggles para verificar si una funcionalidad está activa o no. Esto es: en el punto de entrada a la funcionalidad1 tengo que ver si la misma está disponible o no. Para esto utilizamos la clase featureManager
Dependiendo de cómo esté definido el toogle puede que tengamos que adicionalmente darle al FeatureManager información contextual para que pueda consultarse la funcionalidad. El componente de Microsoft ofrece también featureGates que pueden ser utilizados en los controllers/actions de una aplicación web y así evitar la ejecución en caso que la funcionalidad indicada no esté habilitada.
[FeatureGate("funcionalidad1)]
public class MyController : Controller
{
…
}
Este componente ofrece out-the-box la capacidad para definir evaluación de toggles dependindo de rangos horarios, usuarios y grupos de usuarios. En caso que esto no sea suficiente también es posible crear extensiones para definir criterios propios de evaluación.
Hace un par de semanas en un reunión de trabajo mi colega Mariano explicó la práctica de Continuous Delivery como una analogía con una cinta transportadora y me pareció simplemente excelente.
Siendo estrictos con las definiciones Continuous Delivery implica Continuous Integration y Trunk-Based Development. Entonces:
El equipo hace commits pequeños, si hace TDD, posiblemente un commit por cada nueva prueba en verde.
Un funcionalidad/story requiere de varios commits.
Si el equipo trabaja simultáneamente en más de una funcionalidad es común que una funcionalidad se complete mientras otra está aún en desarrollo.
Si efectivamente hacen continuous delivery es posible que a penas se complete una funcionalidad se la quiera poner producción. Esto a su vez implicaría llevar a producción también las funcionalidades aún no completas.
De esta forma el proceso de continuous delivery es una cinta transportadora donde cada commit es como un paquete que uno pone en la cinta y la cinta lo lleva a producción.
Esto tiene un impacto muy grande en la forma del trabajo del equipo que obliga a tomar precauciones adicionales como puede ser el uso de feature toggles, de manera que si una funcionalidad no completa llega a producción, la misma pueda ser «toggleada/apagada».
Desde hace un tiempo estoy trabajando un equipo de producto de más de 20 personas y estamos organizados en una estructura que es nueva para mi pero que al mismo tiempo me parece viene resultando efectiva. La siguiente figura resume esta estructura.
Si bien todo el equipo de producto son más de 20 personas, en el día a día estamos divididos en 4 equipos: 3 feature teams y 1 equipo de toolsmiths (este nombre lo tomé del enfoque FDD, pero no es el nombre real del equipo)
Los 3 feature teams trabajan en funcionalidades del producto y están en el día a día con gente de negocio.
El equipo de toolsmiths es donde yo trabajo. Actuamos como proveedores/facilitadores de los features teams y nos encargamos de mantener/optimizar la infraestructura de desarrollo: repositorios, build server, ambientes, etc. Incluso en ocasiones agregamos código al producto, pero obviamente relacionado a cuestiones de soporte/infra como feature flags, health endpoints, etc. Como parte de nuestro trabajo tenemos que interactuar con otros grupos de soporte organizacional que se encargan de infraestructura de base y regulaciones (seguridad, red, etc). En el equipo somos 3 personas con skills mixtos: podemos codear, agregar tests, configurar servers, scriptear infraestructura pero sobre todo podemos hablar con otros grupos y generar concenso. Alguien podría tentarse de decir que somos DevOps, pero no lo vemos así, porque de entrada no creemos que DevOps sea ni un rol ni un área, sino un mindset con un conjunto de prácticas.
No se si este es el mejor enfoque para la estructura de un equipo de producto, pero consideramos que nos viene resultando bien: el negocio está contento, los distintos equipos están contentos, el producto está estable y tenemos la capacidad de ir a producción bajo demanda y de forma altamente automatizada.
A partir de la situación inicial previamente descripta, tuvimos que hacer algunos ajustes adicionales a lo esperado pero finalmente logramos paralelizar la pruebas de aceptación.
Logramos una mejora de ~30 % en el tiempo de ejecución del pipeline completo. La mejora es buena pero creemos que podemos mejorarlo aún más ya que en este momento los dos sets de pruebas automatizadas que hemos paralelizado se ejecutan dentro del mismo nodo de Jenkins compitiendo por recursos físicos.
Una mejora posible a esto es agregar un nuevo nodo Jenkins que permita que cada set de pruebas se ejecute con recursos físicos independientes.
Finalmente hay otra serie de posibles mejoras a experimentar pero que requieren trabajo ya a nivel código y por ello son más intrusivas en el día a día del equipo de desarrollo.
Esta semana empezamos a trabajar con @CodeRaguet y Maxi Cruz en la tarea de optimizar el pipeline de deployment. Actualmente tenemos un pipeline de deployment que hace lo siguiente:
Ante cada cambio en el repositorio obtiene el código
Despliega a un ambiente «de demo» donde la aplicación queda disponible para pruebas manuales
La ejecución de este pipeline insume actualmente ~45 minutos y el ~85 % de ese tiempo de ejecución lo consume el paso 4: la ejecución de los tests de aceptación.
Este pipeline representa la primera parte del camino hacia producción y se ejecuta varias veces al día ya que el equipo hace trunk-based development (todos trabajan en master). A continuación de este pipeline hay otro que se dispara manualmente que cubre los ambientes de tests y producción.
Un detalle no menor de esta situación es que si no hacemos nada esta situación va a empeorar gradualmente porque el producto está en constante evolución y todo el tiempo se van a agregando nuevos tests.
El desafió que enfrentamos es reducir el tiempo que insume este pipeline y por ende el tiempo que insume la ejecución de las pruebas aceptación. La estrategia que vamos a intentar implementar es: segmentar los tests de aceptación por grupos de funcionalidades y luego ejecutar cada grupo en paralelo.
Al implementar un pipeline de deployment suele surgir la necesidad de usar algunos parámetros como ser credenciales para conexión a un server, alguna API Key para interactuar con un cloud provider, etc.
Hay varias formas de manejar esto cuando trabajamos con Jenkins. Hoy quiero compartir una forma que me parece elegante y simple a la vez.
La propuesta es utilizar el soporte de Credenciales que ofrece Jenkins. Para esto vamos a Credentials > Global > Add Credentials.
En la pantalla de agregado de Credenciales elegimos el tipo de credencial que queremos agregar. En este ejemplo voy a elegir una credencial del tipo Secret Text pues quiero guardar un simple API token.
Con esto ya hemos almacenado nuestra credencial en Jenkins. El campo ID es el que utilizaremos a posteriori para referenciar esta credencial.
La forma de usar esta credencial varia dependiendo del tipo de credencial (en este caso es Secret Text) y del «lugar» desde donde querramos accederla por ejemplo si es un FreeStyle job o un Pipeline job.
Para usar esta credencial desde un pipeline job se puede usar el siguiente snippet.
Para usar este credencial desde un FreeStyle job, primero bindeamos la credencial a una variable y luego utilizamos directamente la variable.
Un detalle no menor de esta estrategia es que cuando se ejecuta el job el valor de la credencial es enmascarado.
Jenkins es una de las herramientas de CI/CD más difundidas, posiblemente por su potencia y también por su larga historia (su primera versión es de 2005). Yo empecé a utilizarlo allá por 2008 cuando aún era Hudson. Inicialmente era un servidor de integración continua. Luego con el auge de la entrega continua se lo empezó a utilizar para hacer deployments. Esta última característica cobró más impulso a partir de la versión 2.
CircleCI es una herramienta de aparición mucho más reciente y viene a resolver el mismo problema que Jenkins pero con un modelo distinto.
En este post decidí hablar concretamente de Jenkins y Circle porque hoy en día estoy trabajando en dos proyectos, cada uno con una de estas herramientas. Pero en realidad es que en lugar de Circle podría referirme a Travis o a los Builders de Gitlab, pues conceptualmente representan el mismo modelo. Del mismo modo, en lugar de Jenkins podría hablar de Bamboo.
Veamos entonces algunas diferencias de estos dos modelos. CircleCI funciona atado a un repositorio Git (puede ser GitHub, BitBucket o algún otro). Al mismo tiempo, el funcionamiento de la herramienta está definido por un archivo (en formato yaml) donde uno define sus Jobs/Workflows. Si bien la herramienta provee una interface gráfica web, la misma es principalmente para visualización y solo permite ajustar algunos pocos settings, pero no permite la creación de Jobs. La situación es análoga al utilizar Travis o GitLab. Este modelo va muy bien con las estrategias del tipo GitOps, donde todo el pipeline de delivery se articula a partir de operaciones/eventos Git.
Por otro lado, Jenkins ofrece un modelo distinto, uno puede crear jobs asociados a distintos tipos de repositorios o incluso permite crear jobs sin asociación alguna a repositorios. Al mismo tiempo ofrece una interface de usuario web que permite manipular completamente la herramienta, de hecho, durante mucho tiempo esta era la única opción. Luego fueron apareciendo distintas opciones/plugins que posibilitaron tener un manejo similar al de Circle/Travis.
En otra dimensión de análisis podríamos decir que el modelo de Circle/Travis es en cierto modo «CI/CD as a Service». Son herramientas muy enfocadas en CI/CD con toda una serie de cuestiones de diseño ya tomadas de antemano. De hecho en general el modelo de extensibilidad de estas herramientas es bastante limitado.
Por su parte Jenkins surgió inicialmente en modelo más On-Premises/Producto, contando desde su inicio con un modelo muy potente de extensibilidad y luego fue evolucionando incorporando características de los modelo más SaaS. La arquitectura extensible de Jenkins ha posibilitado la aparición de nuevos proyectos montados sobre Jenkins como ser JenkinsX.
Personalmente me siendo muy a gusto trabajando con Jenkins, creo que es un producto muy versátil y lo he utilizado para algunas cosillas más allá de CI/CD. Al mismo tiempo que creo en un contexto organizacional Jenkins ofrece varias características y posibilidades de extensibilidad no presentes en el otro modelo. Entre las bondades de Jenkins podría nombrar la integración con muchísimas herramientas, las capacidades de templating, slicing configuration y la posibilidad de generar plugins en caso que uno lo necesite (lo he hecho y no pareció complicado). Sin embargo y a pesar de mi gusto por Jenkins, creo que la característica de Pipeline as Code (jenkinsfile) no está lo suficientemente lograda. Perdón, creo que la funcionalidad está bien, pero personalmente me siento más a gusto utilizando un set de plugins tradicionales (como JobDSL, el JobBuilder y BuildPipeline) que usando los Jenkinsfile.
Esta tarde a partir de las 18.50 estaré en la oficinas de RyanAir Madrid, exponiendo sobre esta temática.
De paso, voy a aprovechar el contacto con la comunidad ágil de Madrid para hacer difusión de algunos libros «Made in Argentina». Voy a estar sorteando entre los participantes algunos ejemplares de «Construcción de Software, una mirada ágil» y «Experiencias ágiles, relatos de experiencias del uso de métodos ágiles en Argentina».
Si están por Madrid y quieren sumarse, la entrada es gratis pero es necesario registrarse aquí.