De Gitlab a Azure Kubernetes Service

IMPORTANTE: escribo este post principalmente como una nota personal para futura referencia. El procedimiento que describo aquí es el que seguimos para hacer el setup de la infraestructura que utilizamos para el trabajo final de memo2@fiuba. En este contexto de cara a no tener dependencia fuerte con ningún proveedor hemos decidido armar nuestra infraestructura sin hacer uso de ninguna característica particular de las herramientas de ningún vendor. Es por eso que este procedimiento podría utilizarse con mínimos cambios para conectar con cualquier otro proveedor de Kubernetes y al mismo tiempo también podrían utilizarse con otra herramienta de CI/CD que no sea GitLab. En este caso estamos utilizando las suscripciones educativas tanto de Azure como de GitLab.

Creación de cluster

Al crear el cluster simplemente elegimos el tamaño de nodo, en nuestro caso B2S y especificamos 2 nodos. Adicionalmente debemos especificar un grupo para los recursos que se crearán como parte del cluster, en nuestro caso pusimos «memo2» y obviamente necesitamos un nombre para cluster, en nuestro caso «cluster-memo2-prod». Finalmente desactivamos la funcionalidad de monitoreo porque está fuera del scope de nuestra suscripción. Esto representa un costo mensual aproximado de ~ us$ 73, lo cual está bien ya que la suscripción nos ofrece 100 dólares de crédito y nosotros solo necesitamos el cluster 1 mes.

Una vez creado el cluster, el siguiente paso es descargar la configuración de conexión para kubectl (el cliente kubernetes). Para esto es necesario en primera instancia utilizar el azure-cli. La opción obvia es instalar el azure-cli, pero también está la opción de usar un «cloud shell» de azure que nos abre un shell en la ventana del navegador y que tiene el azure-cli instalado. Una vez que tenemos el azure-cli a mano (ya sea instalado en nuestra máquina o usando el cloud shell) debemos ejecutar los siguientes dos comandos:

az account set --subscription <subscription id>
az aks get-credentials --resource-group <resource group> --name <cluster name>

El segundo comando «instala» la configuración para conectarnos al cluster, lo cual significa que tenemos un archivo en ~/.kube/config con todos los parametros de configuración de conexión. Ejecutando «kubectl version» deberíamos ver la versión de nuestro kubectl y la versión de kubernetes que corre el cluster.

Con esto ya estamos en condiciones de conectar nuestro GitLab con el cluster creado.

Conexión Gitlab > Kubernetes

El primer paso, es crear una cuenta de servicio para que gitlab se conecte al cluster. Esto lo hacemos con este manifiesto.

Una vez creada la cuenta de servicio necesitamos obtener su token para lo cual ejecutamos los siguientes comandos:

# primero creamos la cuenta de servicio
kubectl apply -f gitlab-service-account.yaml

# luego buscamos entre los secrets el correspondiente a la cuenta creada
kubectl get secrets

# finalmente hacemos un describe del secret para poder obtener su token
kubectl describe secret <nombre del secret>

Ya con todo esto podemos proceder la configuración del cluster en GitLab

En el formulario de configuración del cluster completamos los campos de la siguiente forma:

  • Kubernetes cluster name: el nombre con el cual queremos identificar el cluster, en mi caso «cluster-memo-prod»
  • Environment scope: el ambiente que este cluster representará, en mi caso será «prod». Este ambiente luego será referenciado dentro del pipeline de CI/CD
  • API URL: la sacamos de la configuración de kubectl, es el campus server. Lo podemos obtener haciendo «cat ~/.kube/config | grep server«
  • CA Certificate: también lo obtenemos de la configuración de kubectl haciendo «cat ~/.kube/config | grep certificate-authority-data«. Pero, adicionalmente hay que desencodearlo, entonces podemos poner el valor en una variable y ejecutar: «echo $CERT | base64 -d«
  • Service token: aquí utilizamos el token de la cuenta de servicio que creamos previamente
  • RBAC-enabled cluster, GitLab-managed cluster, Namespace per environment: en el caso de MeMo2 no hacemos uso de ninguna de estas funcionalidades con los cual dejamos las 3 opciones como unchecked
  • Project namespace prefix (optional, unique): esto tampoco lo utilizamos con lo cual no dejamos en blanco

Deploy de contenedores a Heroku con GitLab

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

package_job:
  stage: package
  image: docker:stable
  before_script:
    - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY
  script:
    - VERSION=$(cat VERSION.txt)
    - docker build -f Dockerfile.prod -t $REGISTRY_URL/$APP_NAME:$VERSION .
    - docker tag $REGISTRY_URL/$APP_NAME:$VERSION $REGISTRY_URL/$APP_NAME:latest
    - docker push $REGISTRY_URL/$APP_NAME:$VERSION
    - docker push $REGISTRY_URL/$APP_NAME:latest

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.

deploy_dev_job:
  stage: deploy
  image: docker:stable
  before_script:
    - apk add curl
    - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY
    - docker login -u _ -p $HEROKU_TOKEN registry.heroku.com
  script:
    - VERSION=$(cat VERSION.txt)
    - docker pull $REGISTRY_URL/$APP_NAME:$VERSION
    - docker tag $REGISTRY_URL/$APP_NAME:$VERSION registry.heroku.com/$HEROKU_APP/web
    - docker push registry.heroku.com/$HEROKU_APP/web
    - export IMAGE_ID=`docker inspect registry.heroku.com/$HEROKU_APP/web --format={{.Id}}`
    - ./scripts/deploy_app.sh

Finalmente este es el script que invoca a la API de Heroku (este script es algo que definitivamente puede mejorarse)

#!/bin/sh
set -x 

DATA='{ "updates": [ { "type": "web", "docker_image":"'
DATA="$DATA$IMAGE_ID"
DATA=$DATA'" } ] }'

curl -X PATCH https://api.heroku.com/apps/$HEROKU_APP/formation --header "Content-Type: application/json" --header "Accept: application/vnd.heroku+json; version=3.docker-releases" --header "Authorization: Bearer ${HEROKU_TOKEN}" --data "$DATA" --fail

Cierro con algunos comentarios:

  • 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.

Infra definida para el TP2 de MeMo2 @ fiuba

Después de varias averiguaciones y algunas pruebas de concepto ya tenemos bastante encaminado el diseño del pipeline e infraestructura del TP2. El sistema a construir consta de dos aplicaciones/artefactos: un bot de telegram y una web-api.

El bot de telegram lo vamos a correr en kubernetes, más precisamente en el servicio de Kubernetes de Azure utilizando la opción Azure for Students ofrecida por Microsoft, la cual incluye 100 dólares de crédito y no requiere de tarjeta de crédito.

Las web-api la vamos a correr en Heroku pero en lugar de usar el modelo de runtime tradicional de Heroku, vamos a correr la aplicación en un contenedor. Esto es: en lugar de hacer push del código fuente directo a Heroku, lo que hacemos es construir una imagen Docker y luego indicar a Heroku que corra un contenedor basado en esa imagen.

Si bien, en términos de infraestructura a bajo nivel, el bot y la api van a correr en distintos runtimes, a nivel proceso ambas aplicaciones correran como un contenedor Docker. Al mismo tiempo el proceso de build y deploy va a ser el mismo para ambas aplicaciones, ofreciendo al equipo de desarrollo una experiencia de trabajo uniforme. En un escenario real es poco probable utilizar una estrategia de este estilo, porque tener dos plataformas de runtime implica un mayor costo operacional pero en nuestro contexto creemos que puede resultar interesante para mostrar explícitamente a los estudiantes como, a partir de ciertas técnicas, es posible lograr un buen nivel abstracción de la infraestructura.

El pipeline de CI/CD lo implementaremos con GitLab utilizando la suscripción Gold que GitLab ofrece para contextos educativos.

Un detalle que me parece relevante mencionar es que, si bien vamos a utilizar productos/servicios de determinados vendors, tenemos la intención de mantener la menor dependencia posible con caraterísticas específicas/propietarias de cada vendor.

En siguientes artículos explicaré como será el modelo de ambientes y pipelines que armaremos tomando esta infraestructura de base.