DevOps y Escalado

Implementación azul-verde para la infraestructura de resolución de CAPTCHA

El despliegue azul-verde consiste en mantener dos entornos idénticos de workers de resolución —uno activo y otro en espera— y conmutar todo el tráfico de uno a otro en segundos. Para una flota que resuelve CAPTCHA de forma continua, esa es la diferencia entre subir la versión de tus workers sin que un solo scraper note el cambio y arriesgarte a un corte a mitad de una cola de tareas. Si la nueva versión falla, vuelves al entorno anterior de inmediato, sin reconstruir nada.

Cuándo conviene aplicarlo a tu resolución de CAPTCHA

Piensa en una agencia de datos en Madrid o Ciudad de México que ejecuta scrapers de monitoreo de precios las 24 horas y factura su infraestructura en USD. Cada minuto de inactividad detiene la recolección y deja tareas a medias. Cuando toca subir una nueva versión del worker no puedes permitirte parar la cola. El patrón azul-verde te deja probar esa versión con tráfico real en el entorno en espera y promoverla solo cuando supera las pruebas canary.

Son justo los cambios que más miedo dan de desplegar en caliente:

  • Un ajuste en la lógica de sondeo o en los intervalos de res.php.
  • Una actualización de dependencias del worker.
  • Un cambio en el manejo de errores o en los reintentos.
  • Un nuevo reparto de threads entre colas.

A diferencia de un despliegue directo, aquí el rollback no implica volver a desplegar: el entorno anterior sigue vivo y basta con reapuntar el router. Eso encaja bien con un servicio como CaptchaAI, donde ambos entornos comparten la misma API key y el mismo grupo de threads, así que duplicar el entorno no significa duplicar tu plan.

Si tus scrapers recolectan datos de terceros, respeta siempre los términos de servicio del sitio y la normativa de protección de datos aplicable (GDPR y LOPDGDD en la UE, LFPDPPP en México). El patrón azul-verde es una técnica de despliegue, no un permiso para saltarte esas reglas.

Arquitectura azul-verde

                    ┌─────────────────────┐
[Scraper Clients] → │   Traffic Router    │
                    └──────┬──────┬───────┘
                           │      │
                     Active│      │Standby
                           ▼      ▼
                    ┌───────┐  ┌───────┐
                    │ BLUE  │  │ GREEN │
                    │Workers│  │Workers│
                    └───┬───┘  └───┬───┘
                        │          │
                        └────┬─────┘
                             ▼
                    [CaptchaAI API]

El router es la única pieza que decide a qué entorno va cada tarea. Sus responsabilidades son mínimas y deliberadas:

  • Mantiene el puntero al entorno activo y al de espera.
  • Envía cada solicitud nueva al activo sin bloquear.
  • Cambia el puntero de forma atómica cuando ordenas el corte o el rollback.

Qué validar antes, durante y después del corte

Antes de tocar el router, confirma que azul y verde son realmente idénticos salvo por la versión que estás probando. Esta lista mínima evita los fallos más habituales de un corte de tráfico:

Fase Qué validar Señal para hacer rollback
Antes del corte Misma configuración, proxies, secretos y límites de threads en azul y verde Diferencias entre variables de entorno o rutas
Canary 5-10 % Latencia, CAPCHA_NOT_READY, tasa de error y consumo por worker Tasa de error por encima de la línea base + 2 %
Corte total El entorno verde sostiene toda la carga al menos un ciclo completo de tráfico Picos de timeout, saldo inconsistente o errores por sesión
Post-corte Azul queda listo para el rollback con las colas drenadas y las métricas en vivo No poder volver atrás en menos de 5 minutos

Cómo implementarlo en código

Los dos ejemplos siguientes implementan el mismo ciclo, uno en cada lenguaje:

  1. Enrutar cada tarea al entorno activo.
  2. Enviar el CAPTCHA a in.php y sondear el resultado en res.php.
  3. Correr las pruebas canary sobre el entorno en espera.
  4. Conmutar el tráfico y vigilar la tasa de error tras el corte.

El primer bloque define el enrutador y las piscinas de workers en Python; el segundo automatiza el corte con pruebas canary y monitoreo posterior en JavaScript.

Python: router azul-verde con pruebas canary

import os
import time
import threading
import requests

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


class CaptchaWorkerPool:
    """Represents one environment (blue or green)."""

    def __init__(self, name, config):
        self.name = name
        self.config = config
        self.session = requests.Session()
        self.tasks_solved = 0
        self.errors = 0
        self.healthy = True

    def solve(self, task):
        resp = self.session.post("https://ocr.captchaai.com/in.php", data={
            "key": API_KEY,
            "method": task.get("method", "userrecaptcha"),
            "googlekey": task["sitekey"],
            "pageurl": task["pageurl"],
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            self.errors += 1
            return {"error": data.get("request")}

        captcha_id = data["request"]
        for _ in range(60):
            time.sleep(5)
            result = self.session.get(
                "https://ocr.captchaai.com/res.php",
                params={
                    "key": API_KEY,
                    "action": "get",
                    "id": captcha_id,
                    "json": 1
                }
            ).json()
            if result.get("status") == 1:
                self.tasks_solved += 1
                return {"solution": result["request"]}
            if result.get("request") != "CAPCHA_NOT_READY":
                self.errors += 1
                return {"error": result.get("request")}

        self.errors += 1
        return {"error": "TIMEOUT"}

    @property
    def error_rate(self):
        total = self.tasks_solved + self.errors
        return self.errors / total if total > 0 else 0.0

    @property
    def stats(self):
        return {
            "name": self.name,
            "solved": self.tasks_solved,
            "errors": self.errors,
            "error_rate": round(self.error_rate, 4),
            "healthy": self.healthy
        }


class BlueGreenRouter:
    def __init__(self, blue_config, green_config):
        self.blue = CaptchaWorkerPool("blue", blue_config)
        self.green = CaptchaWorkerPool("green", green_config)
        self.active = self.blue
        self.standby = self.green
        self.lock = threading.Lock()

    def solve(self, task):
        """Route task to the active environment."""
        with self.lock:
            pool = self.active
        return pool.solve(task)

    def switch(self):
        """Swap active and standby environments."""
        with self.lock:
            self.active, self.standby = self.standby, self.active
            print(f"Switched: {self.active.name} is now ACTIVE")
        return self.active.name

    def rollback(self):
        """Switch back to the previous environment."""
        return self.switch()

    def canary_test(self, test_tasks, threshold=0.9):
        """Run test tasks on standby before switching."""
        successes = 0
        for task in test_tasks:
            result = self.standby.solve(task)
            if "solution" in result:
                successes += 1

        success_rate = successes / len(test_tasks) if test_tasks else 0
        passed = success_rate >= threshold
        print(
            f"Canary test: {successes}/{len(test_tasks)} "
            f"({success_rate:.0%}) — {'PASS' if passed else 'FAIL'}"
        )
        return passed

    @property
    def status(self):
        return {
            "active": self.active.stats,
            "standby": self.standby.stats
        }


# Usage
router = BlueGreenRouter(
    blue_config={"version": "1.2.0", "workers": 4},
    green_config={"version": "1.3.0", "workers": 4}
)

# Canary test before switching
test_tasks = [
    {"sitekey": "6Le-wvkS...", "pageurl": "https://example.com/test"}
]

if router.canary_test(test_tasks, threshold=0.8):
    router.switch()
    print(f"Now active: {router.status['active']['name']}")
else:
    print("Canary failed — staying on current environment")

JavaScript: conmutador con monitoreo posterior al corte

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

class BlueGreenDeployment {
  constructor() {
    this.environments = {
      blue: { name: "blue", version: null, solved: 0, errors: 0 },
      green: { name: "green", version: null, solved: 0, errors: 0 },
    };
    this.activeEnv = "blue";
  }

  get active() {
    return this.environments[this.activeEnv];
  }
  get standby() {
    return this.environments[this.activeEnv === "blue" ? "green" : "blue"];
  }

  async deploy(version, config = {}) {
    const target = this.standby;
    target.version = version;
    target.solved = 0;
    target.errors = 0;

    console.log(`Deployed v${version} to ${target.name} (standby)`);

    // Run canary checks
    const canaryPassed = await this.canaryCheck(config.canaryTasks || []);
    if (!canaryPassed && config.canaryTasks?.length > 0) {
      console.log("Canary check failed — aborting deployment");
      return { success: false, reason: "canary_failed" };
    }

    // Switch traffic
    this.activeEnv = target.name;
    console.log(`Switched traffic to ${target.name} (v${version})`);

    // Monitor for rollback
    if (config.monitorDuration) {
      const stable = await this.monitorAfterSwitch(config.monitorDuration);
      if (!stable) {
        this.rollback();
        return { success: false, reason: "post_deploy_errors" };
      }
    }

    return { success: true, active: this.activeEnv };
  }

  async canaryCheck(tasks) {
    if (tasks.length === 0) return true;

    let successes = 0;
    for (const task of tasks) {
      try {
        await this.solveCaptcha(task);
        successes++;
      } catch (err) {
        console.log(`Canary task failed: ${err.message}`);
      }
    }

    const rate = successes / tasks.length;
    console.log(`Canary: ${successes}/${tasks.length} (${(rate * 100).toFixed(0)}%)`);
    return rate >= 0.8;
  }

  async monitorAfterSwitch(durationMs) {
    const start = Date.now();
    const checkInterval = 10000;

    while (Date.now() - start < durationMs) {
      await new Promise((r) => setTimeout(r, checkInterval));
      const errorRate = this.active.errors /
        Math.max(1, this.active.solved + this.active.errors);

      if (errorRate > 0.2) {
        console.log(`Error rate ${(errorRate * 100).toFixed(1)}% — triggering rollback`);
        return false;
      }
    }
    return true;
  }

  rollback() {
    const previous = this.activeEnv === "blue" ? "green" : "blue";
    console.log(`Rolling back: ${this.activeEnv} → ${previous}`);
    this.activeEnv = previous === "blue" ? "blue" : "green";
  }

  async solveCaptcha(task) {
    const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: task.sitekey,
        pageurl: task.pageurl,
        json: 1,
      },
    });

    if (submitResp.data.status !== 1) {
      this.active.errors++;
      throw new Error(submitResp.data.request);
    }

    const captchaId = submitResp.data.request;
    for (let i = 0; i < 60; i++) {
      await new Promise((r) => setTimeout(r, 5000));
      const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
        params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
      });

      if (pollResp.data.status === 1) {
        this.active.solved++;
        return pollResp.data.request;
      }
      if (pollResp.data.request !== "CAPCHA_NOT_READY") {
        this.active.errors++;
        throw new Error(pollResp.data.request);
      }
    }
    this.active.errors++;
    throw new Error("TIMEOUT");
  }
}

// Deploy new version with canary and monitoring
const deployer = new BlueGreenDeployment();

deployer
  .deploy("1.3.0", {
    canaryTasks: [
      { sitekey: "6Le-wvkS...", pageurl: "https://example.com/test" },
    ],
    monitorDuration: 60000, // Monitor for 1 minute after switch
  })
  .then((result) => console.log("Deploy result:", result));

Flujo de trabajo del despliegue

Resumido en cinco pasos, un corte azul-verde para tus workers de resolución se ve así:

Paso Acción Disparador de rollback
1 Despliega el nuevo código en el entorno en espera Fallo de compilación
2 Ejecuta pruebas canary en el entorno en espera Tasa de éxito < 80 %
3 Conmuta el tráfico a la nueva versión
4 Vigila la tasa de error durante 5 minutos Tasa de error > 20 %
5 Desmantela el entorno anterior

Solución de problemas

Problema Causa Solución
El canary pasa pero producción falla Tareas de prueba demasiado simples Usa tareas reales tomadas de la cola de producción
Rollbacks demasiado frecuentes Umbrales de monitoreo agresivos Sube el umbral de error y alarga el período de estabilización
El tráfico no se divide limpio durante el cambio Solicitudes en vuelo sobre el entorno anterior Espera a que se drenen las tareas en vuelo antes de desmantelar
Ambos entornos quedan degradados Fallo en una dependencia compartida (red, API) Aplica un disyuntor; no hagas rollback por problemas de infraestructura

Preguntas frecuentes

¿Cómo evito perder las tareas en vuelo durante el cambio?

Deja que el entorno anterior termine las solicitudes que ya están en curso antes de desmantelarlo. Conmuta solo las tareas nuevas al entorno verde y espera a que la cola del azul se drene; así ningún CAPTCHA a medio resolver se pierde en el corte.

¿Necesito duplicar mi plan de CaptchaAI para tener dos entornos?

No. CaptchaAI factura por thread concurrente, no por resolución, y ambos entornos pueden compartir la misma API key. Solo necesitas threads suficientes para la carga combinada durante los segundos en que azul y verde coinciden; el plan BASIC ($15/mes, 5 threads) cubre pruebas pequeñas y escalas subiendo de tier cuando crece el volumen.

¿Cuándo conviene azul-verde y cuándo un despliegue canary?

Azul-verde conmuta el 100 % del tráfico de una vez y es más sencillo de operar. Un despliegue canary sube el tráfico de forma gradual (1 % → 10 % → 50 % → 100 %) y es más seguro en sistemas muy grandes. Puedes combinarlos: usa una fase canary como validación previa al corte azul-verde.

¿Qué señal debe disparar un rollback automático?

La más fiable es la tasa de error del entorno activo justo después del corte: si supera un umbral durante una ventana de observación (por ejemplo, más del 20 % durante 5 minutos), reapunta el router al entorno anterior. Combínala con la latencia y la aparición de CAPCHA_NOT_READY para no reaccionar ante un pico puntual.

Próximos pasos

Consigue tu API key en captchaai.com y prueba un corte azul-verde en un entorno de espera antes de llevarlo a producción: despliega la nueva versión, valídala con pruebas canary y promuévela sin cortar la cola de resolución.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.