DevOps & Scaling

Implantação azul-verde para infraestrutura de resolução de CAPTCHA

Trocar a versão do seu pipeline de resolução de CAPTCHA sem perder nenhuma tarefa em produção é uma questão de manter dois ambientes idênticos no ar ao mesmo tempo — um recebendo tráfego real (azul), outro pronto para assumir (verde) — e só migrar depois que o novo ambiente já provou, com CAPTCHAs reais, que está funcionando.

Diferente de um deploy tradicional, não existe reinicialização "no escuro": o ambiente novo passa por um teste canário antes de receber tráfego real, e a reversão leva o mesmo tempo que a troca — segundos, não minutos.

Neste guia você vai:

  • Entender como o roteador decide qual ambiente recebe o tráfego de produção.
  • Rodar um teste canário automatizado no standby antes de qualquer troca real.
  • Implementar rollback instantâneo em Python e Node.js.
  • Seguir um checklist de 5 passos para a virada de tráfego.

Como funciona a arquitetura azul-verde

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

O roteador de tráfego fica entre os clientes (scrapers, workers de automação) e os dois pools de workers. Só o ambiente ativo recebe tráfego real; o standby fica isolado, disponível apenas para os testes canário — é essa separação que permite reverter em segundos: a troca é só uma mudança de referência no roteador, não um novo deploy.

Se os seus workers rodam perto dos usuários finais — por exemplo, na região sa-east-1 (São Paulo) da AWS, para manter baixa latência com tráfego da América Latina — mantenha blue e green na mesma região. Trocar de região junto com a versão mistura duas variáveis de risco na mesma janela de deploy e dificulta isolar a causa se algo sair errado.

Checklist do fluxo de implantação

Passo Ação Gatilho de rollback
1 Implantar a nova versão só no ambiente standby Falha no build
2 Rodar o teste canário no standby com tarefas reais Taxa de sucesso abaixo de 80%
3 Migrar o tráfego para o ambiente novo
4 Monitorar a taxa de erro por 5 minutos Taxa de erro acima de 20%
5 Desativar o ambiente antigo

Implementação do roteador azul-verde

A implementação abaixo cobre as duas pontas do problema: o pool de workers que efetivamente resolve o CAPTCHA (Python) e o orquestrador que decide quando trocar, testar e reverter (Node.js). Os dois falam com os mesmos endpoints in.php e res.php da CaptchaAI — a diferença está em qual camada guarda o estado de "quem está ativo agora".

Router em Python: pool de workers com métricas de erro

O CaptchaWorkerPool abaixo encapsula um ambiente (blue ou green): ele envia a tarefa, faz o polling do resultado e guarda tasks_solved e errors para alimentar a decisão de troca. O BlueGreenRouter decide qual pool está ativo agora e expõe canary_test para validar o standby antes de qualquer troca real:

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")

O bloco acima já cobre produção em escala pequena com controle manual. Para automatizar deploy, canário e rollback num único fluxo — sem alguém rodando comandos manualmente durante a madrugada —, o exemplo em Node.js a seguir encapsula as três etapas numa única chamada.

Switch automatizado em Node.js: deploy, canário e rollback numa chamada

A classe BlueGreenDeployment faz o deploy no ambiente standby, roda o canário via canaryCheck, só troca o tráfego se a taxa de sucesso passar de 80%, e monitora o ambiente ativo por um período configurável — revertendo sozinha se a taxa de erro passar de 20%:

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));

Runbook de redução gradual de tráfego

  • Mantenha os pools azul e verde em checagens de saúde espelhadas antes de rotear qualquer tráfego real para a pilha nova.
  • Desloque o tráfego em etapas explícitas e exija latência estável e taxa de erro controlada antes de avançar para a próxima etapa.
  • Reverta de imediato se a latência de resolução, a taxa de erro alvo ou a profundidade da fila ultrapassarem o limite combinado com o time — não espere o próximo checkpoint programado.

Perguntas frequentes

Quantos workers preciso para manter os dois ambientes ativos sem estourar meu plano de threads?

Blue e green consomem threads do mesmo plano — os dois ambientes não somam limites separados. No BASIC (US$ 15/mês, 5 threads), 2–3 workers por ambiente já ocupam boa parte da cota durante o teste canário; em ADVANCE (US$ 90/mês, 50 threads) ou PREMIUM (US$ 170/mês, 100 threads) sobra folga para manter os dois lados com workers de reserva mesmo durante a troca.

Por quanto tempo faz sentido rodar o teste canário antes de virar todo o tráfego?

Rode pelo menos 10 resoluções reais de CAPTCHA no ambiente standby antes de considerar a taxa confiável. Para sistemas críticos, direcione 5–10% do tráfego de produção para o standby por cerca de 10 minutos antes da virada completa — um lote pequeno demais mascara problemas que só aparecem sob volume real.

O que acontece com as tarefas que já estavam em andamento no ambiente antigo quando o tráfego migra?

Elas continuam rodando até o res.php devolver a resposta ou o timeout estourar — a troca de tráfego afeta só as tarefas novas. Por isso vale esperar as tarefas em voo esgotarem antes de desativar o ambiente antigo: desligar cedo demais derruba resoluções que já estavam quase prontas.

Dá para montar isso com um proxy reverso simples, sem load balancer dedicado?

Sim. NGINX (ou HAProxy) apontando para a porta do ambiente ativo já resolve num único host, com blue e green como processos ou contêineres separados. O roteador em Python deste guia funciona do mesmo jeito nesse cenário — trocar de ambiente é só redirecionar o proxy para a outra porta.

Erros comuns e como corrigir

Problema Causa provável Correção
Canário passa, mas a produção falha Tarefas de teste simples demais, sem cobrir todos os sitekey em uso Use tarefas reais retiradas da fila de produção, não exemplos fixos
Reversões frequentes Limite de monitoramento agressivo demais para o volume real Aumente o limite de erro e o período de observação antes de decidir
A divisão de tráfego não fica limpa durante a troca Requisições em andamento continuam presas no ambiente antigo Aguarde as tarefas em voo esgotarem antes de desativar o ambiente antigo
Os dois ambientes ficam instáveis ao mesmo tempo Falha de uma dependência compartilhada (rede, API da CaptchaAI) Ative um circuit breaker; não reverta por um problema de infraestrutura que afeta os dois lados
Latência sobe logo após a troca O ambiente novo ainda não aqueceu as conexões (pool "frio") Aqueça o standby com requisições sintéticas antes da troca real

Próximos passos

Configure a chave de API da CaptchaAI e coloque o roteador azul-verde deste guia em produção — comece pela CaptchaAI e faça sua primeira troca de tráfego sem downtime.

Guias relacionados:

Os comentários estão desativados para este artigo.