Performance as Code: Estrategias Avanzadas para Integrar k6 en Pipelines CI/CD

Guía definitiva para implementar testing continuo con k6 en GitHub Actions y Jenkins. Aprende a evitar regresiones de rendimiento con configuraciones reales.

Performance as Code: Estrategias Avanzadas para Integrar k6 en Pipelines CI/CD

Como Senior Performance Analyst con más de una década tratando con sistemas críticos en bancos y aerolíneas, he visto una y otra vez el mismo escenario trágico: una aplicación pasa todas las pruebas funcionales, pasa las pruebas de seguridad, y justo antes del Black Friday o un lanzamiento mayor, el equipo de operaciones descubre que el API de pagos colapsa al 20% de la carga esperada.

En el pasado, esto solucionaba contruyendo scripts gigantescos en LoadRunner o JMeter que solo el “gurú” del rendimiento entendía y que se ejecutaban manualmente una semana antes del release. Hoy en día, esa mentalidad es un suicidio técnico. Necesitamos Performance as Code y Shift-Left Testing.

k6 se ha convertido en la herramienta estándar de oro para esto no solo por su eficiencia, sino porque utiliza JavaScript, el lenguaje universal de los desarrolladores modernos. Pero instalar k6 no es ingeniería de rendimiento; integrarlo inteligentemente en tu pipeline para bloquear despliegues defectuosos sí lo es.

En esta guía, no solo verás cómo “instalar” k6, sino cómo construir una estrategia de guardias de seguridad (Thresholds) y ejecuciones automatizadas que protejan tu calidad de servicio.

El Problema: ¿Por qué el Testing Continuo es Difícil?

Al integrar pruebas de rendimiento en CI/CD, nos enfrentamos a dos enemigos principales:

  1. Falsos Positivos (Flakiness): El pipeline falla porque el servidor de integración estaba ocupado, no porque tu código sea malo. Esto lleva a que los desarrolladores ignoren las alertas.
  2. Tiempo de Ejecución: Una prueba de carga de 2 horas detiene el flujo de entrega continua (CD).

La solución radica en la estratificación de las pruebas.

Nuestro Caso de Estudio: API de E-commerce

Para este tutorial, vamos a probar un API simulada de una tienda online. Tendremos tres objetivos:

  1. Smoke Test (En cada commit): Verifica que el sistema responda.
  2. Functional Load Test (En Pull Requests): Verifica que el rendimiento no haya degradado.
  3. Stress Test (Nocturno): Romper el sistema en un ambiente de staging.

Paso 1: Preparación y Scripting Robusto

Antes de tocar el pipeline, necesitamos un script que sea fiable. No haremos un Hello World. Vamos a simular un flujo completo de usuario: Login -> Búsqueda -> Agregar al Carrito.

Instalación básica (para referencia):

  • MacOS: brew install k6
  • Windows: choco install k6
  • Linux: sudo gpg -k y seguir el repo oficial o usar snap install k6

El Script: checkout_flow.js

Este script introduce Thresholds (Umbrales), que son la parte más crítica para CI/CD. Un threshold define la regla de éxito o fracaso. Si el 95% de las respuestas tardan más de 500ms, el script sale con código de error 1, fallando el pipeline.

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

// Métrica personalizada para rastrear errores de negocio
const errorRate = new Rate('errors');

// Configuración basada en variables de entorno para flexibilidad
const BASE_URL = __ENV.API_URL || 'https://test-api.k6.io';

export const options = {
  // Definimos etapas para ramp-up y ramp-down controlados
  stages: [
    { duration: '30s', target: 20 },  // Ramp-up a 20 usuarios
    { duration: '1m', target: 50 },   // Subida a 50 usuarios (carga pico)
    { duration: '20s', target: 0 },   // Ramp-down
  ],
  thresholds: {
    // Regla estricta de Latencia: El 95% de las peticiones deben ser < 500ms
    http_req_duration: ['p(95)<500'],
    
    // Tasa de errores HTTP debe ser < 1%
    http_req_failed: ['rate<0.01'],
    
    // Nuestros checks de negocio (lógica de aplicación) deben pasar el 95% de las veces
    errors: ['rate<0.05'],
  },
};

export function setup() {
  // Opcional: Podríamos crear datos aquí (ej: crear token de auth)
  return { authToken: 'mock_token_123' };
}

export default function (data) {
  const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.authToken}` };

  // 1. Listar Productos (Browse)
  let res = http.get(`${BASE_URL}/public/crocodiles/`, { headers });
  
  // Verificación de negocio y técnica
  const browseOk = check(res, {
    'status is 200': (r) => r.status === 200,
    'has content': (r) => r.json().length > 0,
  });
  errorRate.add(!browseOk);

  // Simulamos tiempo de lectura
  sleep(Math.random() * 3 + 2);

  // 2. Ver detalle de producto (Checkout Start)
  if (res.json().length > 0) {
    const id = res.json()[0].id;
    res = http.get(`${BASE_URL}/public/crocodiles/${id}`, { headers });
    
    const detailOk = check(res, {
      'detail status 200': (r) => r.status === 200,
    });
    errorRate.add(!detailOk);
  }
}

Este script es modular. Usa __ENV.API_URL, lo que nos permite ejecutarlo contra localhost, dev o staging simplemente cambiando una variable en el pipeline, sin tocar el código.

Paso 2: Integración con GitHub Actions (El Estándar Moderno)

GitHub Actions es ideal para equipos que buscan velocidad. La estrategia aquí es ejecutar un Smoke Test rápido en cada push para validar la sintaxis y conectividad, y una prueba de carga más completa en el pull_request.

El Workflow: .github/workflows/performance.yml

Vamos a usar un enfoque matricial para probar en diferentes condiciones o simplemente organizar los jobs.

name: Performance Test Suite

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main" ]
  workflow_dispatch: # Permitir ejecución manual desde la UI de GitHub

env:
  API_URL: ${{ secrets.API_URL_STAGING }} # Usamos secrets para URLs sensibles
  K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }} # Opcional: si usas k6 Cloud

jobs:
  smoke-test:
    name: Smoke Test (Sanity Check)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6

      - name: Run k6 smoke test
        run: |
          k6 run --out json=report.json \
            --env API_URL=${{ env.API_URL }} \
            --stage 5s:5,10s:5 \
            --threshold 'http_req_duration[0.95]<500' \
            scripts/checkout_flow.js
      # Nota: Este job fallará el PR si los thresholds no se cumplen

  load-test:
    name: Functional Load Test (PR Only)
    runs-on: ubuntu-latest
    needs: smoke-test # Solo corre si el smoke pasa
    if: github.event_name == 'pull_request'
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run k6 with Docker
        uses: grafana/k6-action@v0.3.0
        with:
          filename: scripts/checkout_flow.js
        env:
          # Sobrescribimos opciones de configuración si es necesario
          K6_OPTIONS: "--out influxdb=http://${{ secrets.INFLUXDB_HOST }}:8086/k6 --env API_URL=${{ env.API_URL }}"

      - name: Upload Results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: k6-report
          path: report.json

Análisis de la Configuración

  1. Instalación Dinámica: En el smoke-test instalamos k6 vía apt para tener control total. En load-test usamos la acción oficial de Grafana para Dockerizar la ejecución, lo cual es más limpio y aislado.
  2. Secretos: Nunca hagas hardcode de URLs de producción. Usa secrets.API_URL_STAGING.
  3. Salida de Datos: Observa el flag --out json=report.json y --out influxdb=.... En un pipeline real, no quieres ver el log de texto en consola. Quieres enviar los datos a una base de datos de series temporales (InfluxDB/Prometheus) para graficarlos en Grafana. Esto permite comparar el PR #55 contra el PR #54 visualmente.

Paso 3: Integración con Jenkins (Empresas Legacy)

En clientes grandes como Bank of America o Toyota, Jenkins sigue siendo el rey. Aquí la integración requiere un archivo Jenkinsfile. Usaremos una Declarative Pipeline.

El Jenkinsfile

pipeline {
    agent any
    
    parameters {
        string(name: 'TARGET_ENV', defaultValue: 'staging', description: 'Environment to test')
        string(name: 'VUS', defaultValue: '50', description: 'Number of Virtual Users')
    }

    environment {
        K6_BIN = 'k6'
        REPORT_DIR = 'k6-reports'
    }

    stages {
        stage('Preparation') {
            steps {
                sh 'mkdir -p ${REPORT_DIR}'
                echo "Running performance tests against ${params.TARGET_ENV} with ${params.VUS} VUs"
            }
        }

        stage('Sanity Check') {
            steps {
                script {
                    try {
                        sh "${K6_BIN} run --env API_URL=http://${params.TARGET_ENV}.company.com --stage 10s:5,10s:0 scripts/checkout_flow.js"
                    } catch (Exception e) {
                        // Si falla el sanity check, abortamos el pipeline inmediatamente
                        currentBuild.result = 'FAILURE'
                        error("Sanity Check Failed: ${e.getMessage()}")
                    }
                }
            }
        }

        stage('Load Test Execution') {
            steps {
                sh """
                    ${K6_BIN} run \
                    --out json=${REPORT_DIR}/raw_data.json \
                    --env API_URL=http://${params.TARGET_ENV}.company.com \
                    --stage 1m:10,2m:${params.VUS},1m:10 \
                    scripts/checkout_flow.js
                """
            }
            post {
                always {
                    // Publicamos el reporte HTML en Jenkins
                    publishHTML(target: [
                        reportDir: '${REPORT_DIR}',
                        reportFiles: 'index.html',
                        reportName: 'K6 Performance Report'
                    ])
                }
            }
        }
    }

    post {
        failure {
            emailext (
                subject: "Performance Gate Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "El rendimiento del ambiente ${params.TARGET_ENV} no cumple con los umbrales requeridos.\n\nRevisar: ${env.BUILD_URL}",
                to: 'devops-team@company.com, perf-team@company.com'
            )
        }
    }
}

Detalles Técnicos en Jenkins

  • Parámetros: Permitimos que quien dispare el pipeline elija el entorno (TARGET_ENV) y la carga (VUS). Esto es crucial para pruebas manuales ad-hoc que a veces piden los QAs.
  • Gatekeeping: Si el Sanity Check falla (Stage 2), el pipeline se detiene inmediatamente usando error(). Esto ahorra tiempo y recursos del servidor Jenkins.
  • Reportes: El uso de publishHTML (requiere el plugin HTML Publisher) permite ver un gráfico visual directamente en la interfaz de Jenkins, mucho mejor que leer logs de consola.

Paso 4: Estrategias Avanzadas y Optimización

Manejo de Datos (Test Data Management)

Uno de los mayores dolores de cabeza en CI/CD es crear datos para las pruebas. No puedes ejecutar 1000 usuarios haciendo login con el mismo usuario o creando nuevos registros que ensucian la base de datos.

Solución: Usar JSON con datos pre-cargados o Mocking.

// data.json
["user1", "user2", "user3", ...]

// En el script JS:
const data = JSON.parse(open('./data.json'));

export default function() {
    const user = data[__VU % data.length]; // Asignar usuario basado en ID de VU
    // ...
}

Para CI/CD, prefiero generar este archivo data.json en el stage de “Preparation” del pipeline usando scripts de Python o NodeJS, limpiando y creando datos frescos para esa ejecución específica.

Separación de Ambientes (The 3-Tier Approach)

No ejecutes la misma prueba en todos los sitios.

  1. Commit (Local/Dev): Solo ejecuta k6 validate script.js. Verifica sintaxis. (Costo: ~1s).
  2. Feature Branch (Integration): Ejecuta Smoke Test con 10 VUs durante 30s. Verifica que no rompiste nada obvio. (Costo: ~30s).
  3. Main/Merge (Staging): Ejecuta Load Test completo con 500 VUs. Aquí aplicamos los thresholds estrictos. Si falla aquí, no se despliega a Producción. (Costo: ~10 min).

Troubleshooting: Errores Comunes en Pipelines

En mi experiencia, estos son los problemas que más frenan a los equipos al principio:

1. “Error: open : no such file or directory”

Ocurre cuando haces referencia a archivos dentro del script usando rutas relativas (./data.json), pero el pipeline cambia el directorio de trabajo antes de ejecutar k6 (común en Jenkins). Fix: Usa rutas absolutas o verifica el pwd en el paso anterior del pipeline. Mejor aún, usa __ENV.WORKSPACE y pásalo como variable.

2. Flaky Tests por Network Throttling

El runner de GitHub Actions a veces puede ser lento o tener picos de latencia, haciendo que tus thresholds de 200ms fallen no por tu API, sino por la infraestructura de GitHub. Fix: Ajusta los thresholds para el entorno de CI. Si en producción tu SLA es 200ms, en CI (que corre sobre internet pública) pon el threshold en 500ms o 800ms. El objetivo del CI es encontrar regresiones masivas, no micro-optimizaciones de milisegundos.

3. Resource Exhaustion en el Runner

Si intentas generar 10,000 VUs en un runner gratuito de GitHub Actions o un nodo Jenkins barato, el runner colapsará antes que tu servidor. Fix: Usa k6 Operator con Kubernetes para pruebas de alta carga. Delega la generación de carga a un cluster K8s dedicado, y deja que el pipeline de CI/CD solo sea el “trigger”.

Conclusión

Integrar k6 en CI/CD no es solo automatizar un script; es cambiar la cultura. Pasas de “¿cómo se comportará el sistema?” a “El sistema no se despliega si no cumple con estas métricas específicas”.

Recapitulando los puntos clave para tu implementación:

  1. Thresholds son obligatorios: Sin http_req_duration['p(95)<X'], k6 en CI/CD es inútil.
  2. Modulariza tus scripts: Usa variables de entorno para apuntar a diferentes URLs.
  3. Estratifica: No mates el pipeline con pruebas de 1 hora en cada commit.
  4. Aísla los datos: Ensucia el ambiente de staging, no el de producción.

Con esta configuración, tendrás un sistema de detección temprana que te ahorrará esas llamadas de emergencia a las 3 AM. ¡A codear!