Pruebas de carga programadas con Artillery y Microsoft Azure

Para aquellos escenarios más exigentes, donde pueden verse comprometidos por tener picos importantes de carga durante el día o fechas concretas, siempre es recomendable tener definidas diferentes pruebas de carga que nos permitan saber que nuestro servicio va a responder correctamente, incluso con las nuevas releases que vayamos lanzando. Lo ideal sería que estas pruebas pudieran lanzarse en un entorno controlado, con un horario que fuera poco comprometido para nuestro sistema, que de una forma automatizada nos permita simplemente recibir el informe de cómo fueron las ejecuciones de las mismas. Para cubrir este escenario he montado lo siguiente:

Gracias a GitHub Actions, Terraform, Azure Container Instances, Logic Apps y Azure Functions automatizo las pruebas de carga nocturnas. En este artículo te explico qué hace cada pieza y cómo puedes replicarlo en tu entorno.

El resultado

Antes de contarte el cómo te quiero mostrar cuál será el resultado obtenido:

Correo con el informe de Artillery

El objetivo final es que, simplemente habiendo configurado con Artillery las pruebas de carga que quiero lanzar, obtenga un correo electrónico con el informe que Artillery provee de forma automatizada, en el momento que se lancen las pruebas.

La prueba de carga con Artillery

Por si no lo conoces, Artillery se trata de un framework Open Source que nos permite, a través de una configuración en YAML, definir pruebas de carga. Para este artículo he utilizado la que aparece como ejemplo en la propia documentación, pero obviamente suelen ser más elaboradas:

config:
  target: 'https://artillery.io'
  phases:
    - duration: 60
      arrivalRate: 20
  defaults:
    headers:
      x-my-service-auth: '987401838271002188298567'
scenarios:
  - flow:
    - get:
        url: "/docs"

Para poder ejecutar estas pruebas es necesario instalar esta herramienta a través de un módulo de Node.js en la máquina que se encargará de ello. En este escenario, para poder ejecutarla de forma sencilla en cualquier lugar he dockerizando el proceso. De esta forma nos aseguramos que podemos lanzar y destruir la tarea en cualquier sitio donde tengamos un Docker Engine:

FROM alpine

ENV ARTILLERY_YAML_FILE=''
ENV REPORT_NAME='artillery-report'
ENV AZURE_STORAGE_CONNECTION_STRING=''

WORKDIR /artillery

COPY ./scripts/ . 

RUN apk add --update nodejs npm && \
    npm install -g artillery && \
    apk update && \
    apk add bash py-pip && \
    apk add --virtual=build gcc libffi-dev musl-dev openssl-dev python3-dev make && \
    pip --no-cache-dir install -U pip && \
    pip --no-cache-dir install azure-cli && \
    apk del --purge build

ENTRYPOINT ["/bin/ash","run-tests.sh"]

Como puedes ver, este Dockerfile instala todas las librerías necesarias tanto para Artillery como Azure CLI. Como punto de entrada tiene un archivo llamado run-tests.sh que es como el que sigue:

#!/bin/bash

echo "Executing Artillery load tests from $ARTILLERY_YAML_FILE"

echo "list files inside of /tests folder"

ls /tests

artillery run $ARTILLERY_YAML_FILE -o "${REPORT_NAME}.json"

echo "Creating results file"

NOW=$(date +"%H_%M_%m_%d_%Y")
REPORT_FILE="${REPORT_NAME}-${NOW}.html"

artillery report -o "$REPORT_FILE" "${REPORT_NAME}.json"

#Upload the file
echo $AZURE_STORAGE_CONNECTION_STRING
az storage blob upload -f $REPORT_FILE -c '$web' -n $REPORT_FILE

Este me va a permitir no solo lanzar la prueba mostrada anteriormente, sino que además va a generar el informe que posteriormente voy a subir a una cuenta de almacenamiento. De hecho, en esta cuenta voy a tener configurada la opción Static Website para que pueda servir los archivos HTML que genera Artillery, ya verás por qué 😉 Antes de continuar, si quisieras probar el resultado en local, puedes hacerlo siguiendo estos comandos:

#Create a storage account to upload reports
RESOURCE_GROUP="artillery-load-tests"
LOCATION="northeurope"
STORAGE_NAME="artillerystuff" 

#Log in Azure
az login

#Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION

#Create a storage account
az storage account create --name $STORAGE_NAME --resource-group $RESOURCE_GROUP --location $LOCATION --sku Standard_LRS
#Enable static website
az storage blob service-properties update --account-name $STORAGE_NAME --static-website --404-document 404.html --index-document index.html
#Get the connection string
CONNECTION_STRING=$(az storage account show-connection-string --name $STORAGE_NAME --resource-group $RESOURCE_GROUP | jq .connectionString)

# Build image
docker build -t 0gis0/artillery-on-aci artillery/

# Test locally
docker run \
    -v $(pwd)/tests:/tests \
    -e ARTILLERY_YAML_FILE=/tests/load.yaml \
    -e REPORT_NAME=returngis \
    -e AZURE_STORAGE_CONNECTION_STRING=$CONNECTION_STRING \
    0gis0/artillery-on-aci

Si todo ha ido bien, al finalizar deberías de tener tu informe en la cuenta de almacenamiento que acabas de crear, en el container $web.

Lo chulo es que si utilizas la URL generada, al habilitar el modo Static Web, esta te servirá el archivo si accedes, en este ejemplo, a https://artillerystuff.z16.web.core.windows.net/returngis-19_00_05_09_2021.html. Esto nos será útil para después cuando queramos mandar este informe por correo electrónico.

Azure Container Instances para lanzar la prueba de carga

Ahora que ya has podido comprobar que tu prueba con Artillery contenerizado funciona correctamente (lanza la prueba, genera el informe y lo sube a una cuenta de almacenamiento), necesitamos ubicar esta ejecución en Azure. Para este escenario, donde dependo de Docker para ejecutar la prueba, Azure Container Instances encaja a la perfección: puedo generar contenedores sin tener que preocuparme de la instalación de Docker Engine en una máquina o de la administración de un clúster de Kubernetes. Además me permite de forma sencilla ubicar esta prueba en diferentes partes del mundo, gracias a las regiones que Microsoft Azure tiene a nuestra disposición para desplegar los servicios.

Para probarlo, solamente tengo que tener en cuenta dos cosas: necesito una carpeta compartida en una cuenta de almacenamiento para subir los tests y necesito asociar esta en la creación del contenedor, de la misma forma que mapee la carpeta que tenía en local. En este ejemplo voy a utilizar la misma cuenta de almacenamiento que en el apartado anterior:

### Using ACI ###
SHARE_NAME="load-tests"

#Get Azure storage access key
STORAGE_KEY=$(az storage account keys list -g $RESOURCE_GROUP -n $STORAGE_NAME --query '[0].value' -o tsv)

#Create a File Share
az storage share create --account-name $STORAGE_NAME --name $SHARE_NAME

#Upload test to the file share (se puede montar un repositorio git)
az storage file upload --account-name $STORAGE_NAME --share-name $SHARE_NAME --source tests/load.yaml

#Upload docker image to Docker Hub
docker push 0gis0/artillery-on-aci

#Create Azure Container Instances
CONTAINER_NAME="artillery-on-azure"

az container create --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME \
    --image 0gis0/artillery-on-aci \
    --dns-name-label artillery-on-azure \
    --ports 80 \
    --restart-policy Never \
    --azure-file-volume-account-name $STORAGE_NAME \
    --azure-file-volume-share-name $SHARE_NAME \
    --azure-file-volume-account-key $STORAGE_KEY \
    --azure-file-volume-mount-path /tests \
    --environment-variables ARTILLERY_YAML_FILE=/tests/load.yaml REPORT_NAME=returngis AZURE_STORAGE_CONNECTION_STRING=$CONNECTION_STRING

#See the logs
az container attach --name $CONTAINER_NAME --resource-group $RESOURCE_GROUP

#Delete container group & storage account
az group delete --name $RESOURCE_GROUP -y

Para poder ejecutar mi contenedor en ACI he tenido que subir la imagen de Docker generada anteriormente a algún sitio remoto accesible por el servicio. En este punto he usado Docker Hub, por ser el sitio más cómodo, sin ninguna configuración adicional. He creado una carpeta compartida en la cuenta de almacenamiento, he recuperado la clave de la misma y con todo ello he creado el contenedor que ejecuta la prueba de carga con Artillery. Si quieres ver el proceso puedes conectarte a los logs del contenedor con la opción az container attach. El resultado final debería de ser el mismo que el anterior: un informe generado en el contenedor $web.

Para completar el flujo que te mostré al inicio de este artículo, queda generar una Logic App que mande el informe via correo electrónico.

Logic App para el envío de los informes

Si bien es cierto que todos los informes se van almacenando en el contenedor $web de la cuenta de almacenamiento, y podemos consultarlos cuando queramos, para que no tengamos que ir proactivamente a buscarlos he creado una Logic App que cada vez que un nuevo informe es depositado en este contenedor nos enviará un correo electrónico con el contenido del mismo:

Flujo de Azure Logic App para enviar los informes de Artillery

En este flujo intervienen varios servicios:

  • Event Grid será el encargado de suscribirse a los eventos de creación de la cuenta de almacenamiento que elijamos.
  • Azure Function que tendrá la lógica necesaria para poder generar un pantallazo del informe que se acaba de generar.
  • Una cuenta de Office 365 para enviar el informe (esta puede sustituirse por otro tipo de cuentas si fuera necesario).

Respecto a la Azure Function, he utilizado Puppeteer que nos permite hacer el pantallazo del informe:

const puppeteer = require('puppeteer');

module.exports = async function (context, req) {

    const url = req.query.url || (req.body && req.body.url);
    context.log(`Analyzing URL ${url}`);

    const browser = await puppeteer.launch({
        defaultViewport: { width: 1920, height: 1080 },
        args: ['--no-sandbox']
    });

    const page = await browser.newPage();
    await page.goto(url);
    await page.waitForTimeout(2000);
    const screenshotBuffer = await page.screenshot({ fullPage: true });
    await browser.close();

    context.res = {
        body: `data:image/png;base64,${screenshotBuffer.toString('base64')}`
    };
}

Una cosa curiosa que he tenido que hacer, para que el informe se viera lo mejor posible, es esperar 2 segundos para que se terminara de cargar la animación JavaScript y que se vieran las curvas de las gráficas correctamente 🙂

El código de la Azure Function y la plantilla ARM de la Logic App son parte del repositorio GitHub de este ejemplo. Sin embargo todo ello se despliega mucho mejor con Terraform.

Despliegue con Terraform

Ahora que ya tenemos todas las piezas, lo último que nos queda es automatizar su creación. Si todos estos servicios los puedo desplegar como Infraestructura como código todo va a ser mucho más rápido. Normalmente uso Terraform y gracias a ello soy capaz de desplegar y versionar la configuración de las diferentes piezas. Para este ejemplo el despliegue debe seguir un orden, ya que unas piezas dependen de otras para poder funcionar.

  1. Azure Function

La primera parte a desplegar sería la Azure Function que usará la Logic App para recuperar el pantallazo:

#### Terraform ####

STORAGE_ACCESS_KEY="YOUR_STORAGE_ACCOUNT_ACCESS_KEY"

#1. Create Puppeteer Deployment
cd tf-puppeteer
terraform init \
    -backend-config="storage_account_name=statestf" \
    -backend-config="container_name=artillery" \
    -backend-config="key=puppeteer.tfstate" \
    -backend-config="access_key=$STORAGE_ACCESS_KEY"

terraform validate
terraform plan -out puppeteer.tfplan
terraform apply puppeteer.tfplan

#Deploy Puppeter
AZURE_FUNC_NAME=$(terraform output -raw azure_function_name)

#Install Azure Functions Tools if you don't have it
brew tap azure/functions
brew install [email protected]

#Deploy code on Azure Functions
cd ..
cd puppeteer-func
func azure functionapp publish $AZURE_FUNC_NAME --javascript --build remote

En este punto no solo es necesario crear el servicio en sí, sino que además necesitamos desplegar el código fuente ubicado en puppeteer-func.

2. Logic App

Tal y como te expliqué en el artículo sobre cómo desplegar Logic Apps con Terraform, he seguido los mismos pasos para esta prueba:

# 2. Create Logic App

# API Connections first
cd tf-logic-app/api-connections
terraform init \
    -backend-config="storage_account_name=statestf" \
    -backend-config="container_name=artillery" \
    -backend-config="key=api-connections.tfstate" \
    -backend-config="access_key=$STORAGE_ACCESS_KEY"

terraform validate
terraform plan -out api-connections.tfplan
terraform apply api-connections.tfplan

# Now we can create the workflow
cd tf-logic-app/workflow
terraform init \
    -backend-config="storage_account_name=statestf" \
    -backend-config="container_name=artillery" \
    -backend-config="key=logicapp.tfstate" \
    -backend-config="access_key=$STORAGE_ACCESS_KEY"

terraform validate
terraform plan -var="access_key=$STORAGE_ACCESS_KEY" -out logicapp.tfplan 
terraform apply logicapp.tfplan

Primero genero las API Connections que necesito para el flujo. En este caso son dos con autenticación OAuth por lo que no tengo más remedio que entrar en el portal de Azure y autorizar tanto la conexión con Event Grid como con Office 365 de manera manual, para que mi Logic App funcione correctamente. Una vez hecho esto ya puedo desplegar el flujo que utiliza estas conexiones.

3. Azure Container Instances

Una vez que tenemos montada la parte que nos notifica de que el informe está listo, lo último que tengo que crear, y repetiré tantas veces como sean necesarias en un futuro, es el contenedor que lanza la prueba de carga:

#Create the resource group and ACI container
cd tf-artillery
terraform init \
    -backend-config="storage_account_name=statestf" \
    -backend-config="container_name=artillery" \
    -backend-config="key=artillery-on-azure.tfstate" \
    -backend-config="access_key=$STORAGE_ACCESS_KEY"


terraform validate
terraform plan -out artillery.tfplan
terraform apply artillery.tfplan

Para que esta última parte la podamos repetir varias veces de manera programada he delegado esta tarea en GitHub Actions.

GitHub Actions para automatizar el proceso

Para ponerle la guinda al pastel lo único que nos quedaría sería automatizar y programar todo este proceso. Para ello me he creado tres flujos en GitHub Actions: uno para crear la Azure Function, y actualizarla cada vez que haya cambios en alguno de sus archivos, otra para la Logic App y, la que he programado, es la que crea el proceso en Azure Container Instances:

name: 3. Artillery scheduled
on:
  push:
    branches:
      - main
    paths:
      ["artillery/**", "tf-artillery/**", ".github/workflows/artillery-scheduled.yml","tests/**"]
  workflow_dispatch:
  schedule:
    - cron: "5 0 * * *"

env:
  IMAGE_NAME: artillery-on-azure
  TF_WORK_DIR: ./tf-artillery

jobs:
  build_and_push_to_registry:
    name: Build and push Docker image with Artillery
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/[email protected]
      - name: Login to GitHub Packages
        uses: docker/[email protected]
        with:
          registry: docker.pkg.github.com
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Lowercase repository name
        run: |
          echo "REPO=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
      - name: Short the sha to 7 characters only
        id: vars
        run: echo ::set-output name=tag::$(echo ${GITHUB_SHA::7})
      - name: Build and push to GitHub Packages
        uses: docker/[email protected]
        with:
          context: ./artillery
          file: ./artillery/Dockerfile
          push: true
          tags: docker.pkg.github.com/${{ env.REPO }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
  terraform:
    needs: build_and_push_to_registry
    name: Create infrastructure on Microsoft Azure with Terraform
    runs-on: ubuntu-latest
    env:
      ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
      ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
      ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
      ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }}
    steps:
      - name: Check out the repo
        uses: actions/[email protected]
      - name: Setup Terraform
        uses: hashicorp/[email protected]
        with:
          terraform_wrapper: false #We need this to get the output
      - name: Terraform Init
        run: terraform init --backend-config="storage_account_name=${{ secrets.TF_STATE_STORAGE_NAME }}" --backend-config="container_name=${{ secrets.TF_STATE_CONTAINER_NAME }}" -backend-config="key=artillery-on-azure.tfstate"
        working-directory: ${{ env.TF_WORK_DIR }}
      - name: Terraform Validate
        run: terraform validate
        working-directory: ${{ env.TF_WORK_DIR }}
      - name: Terraform plan
        run: terraform plan
        working-directory: ${{ env.TF_WORK_DIR }}
      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: ${{ env.TF_WORK_DIR }}

En este ejemplo este flujo se lanza todas las noches, pero puedes ajustarlo a través de una expresión cron a cuando más se ajuste en tu escenario. Para que funcionen los flujos de GitHub Actions es necesario tener todos estos secrets dados de alta en el repositorio:

Secrets necesarios para la prueba

Todos los que empiezan por ARM_ son los necesarios para Terraform. Los dos secretos TF_ son el contenedor y el nombre de la cuenta de almacenamiento donde están los estados de Terraform. AZUREAD_DOMAIN es el dominio.onmicrosoft.com donde se van a desplegar los recursos. Para crear el valor de AZURE_CREDENTIALS puedes lanzar el siguiente comando, que creará un service principal con permisos de contributor:

#Get Azure Credentials for GitHub Actions
az ad sp create-for-rbac --name artillery-deployment --role contributor --sdk-auth

Por otro lado, lo que he hecho ha sido crear un par de entornos de GitHub para que cuando se hacen cambios o se despliega la parte de la Logic App se pare momentos antes de lanzar el flujo, con el objetivo de que de tiempo a autorizar las APIs Connections necesarias.

Uso de los entornos para tener tiempo de autorizar las API Connections

El código de esta prueba lo tienes en mi GitHub.

¡Saludos!

logo lemoncode

 

Bootcamp Devops

Si tienes ganas de meterte en el área de Devops, formo parte del
equipo de docentes del Bootcamp Devops Lemoncode, ¿Te animas a
aprender con nosotros?

Más Info