DevOps & Scaling

Déploiement bleu-vert pour l'infrastructure de résolution de CAPTCHA

Pour mettre à jour un pool de workers de résolution de CAPTCHA sans jamais interrompre le trafic, faites tourner deux environnements identiques et basculez de l'un à l'autre en une seule opération. C'est tout le principe du déploiement bleu-vert : l'environnement « bleu » sert la production pendant que le « vert » reçoit la nouvelle version. Vous validez le vert avec quelques résolutions réelles, puis vous redirigez le trafic. Si la nouvelle version dérape, le retour arrière est immédiat — vous repointez le routeur vers le bleu, sans redéploiement ni fenêtre de maintenance.

Faut-il vraiment passer au bleu-vert ?

Le bleu-vert double le nombre d'environnements à opérer. Ce coût ne se justifie que dans certains cas ; ailleurs, une stratégie plus légère suffit.

Votre situation Bleu-vert utile ? Alternative plus légère
Des workers résolvent des CAPTCHA en continu pour plusieurs équipes Oui — le rollback doit être quasi instantané
Vous modifiez la logique de polling, de session ou de proxy Oui — le risque de régression opérationnelle est élevé
Vous déployez rarement et tolérez une courte fenêtre de maintenance Rarement nécessaire Rolling update ou redémarrage piloté
Vous gérez peu de workers avec un trafic faible Non La complexité ajoutée apporte peu de valeur

Côté facturation, une précision utile : CaptchaAI facture au thread concurrent, pas à l'environnement. Faire tourner un bleu et un vert ne double pas votre coût — l'environnement en veille ne consomme aucun thread tant qu'il ne résout rien. Un plan BASIC ($15/mois, 5 threads) suffit largement pour répéter une bascule avant de monter en charge.

Comment fonctionne l'architecture bleu-vert

Un routeur en façade dirige le trafic vers l'environnement actif. Les deux pools de workers pointent vers la même API CaptchaAI ; seule la version du code des workers change entre le bleu et le vert.

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

En pratique, ce routeur est un reverse proxy NGINX ou un load balancer managé — par exemple chez OVHcloud ou Scaleway — placé devant les deux pools. Basculer revient à changer la cible amont ; rien ne touche à vos clés ni à l'API.

Mise en place

Python : le routeur bleu-vert

Ce routeur encapsule chaque environnement dans un CaptchaWorkerPool, expose les statistiques (taux d'erreur, résolutions) et bascule l'actif et la veille sous un verrou. La méthode canary_test rejoue quelques tâches sur la veille avant d'autoriser le changement.

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 : la bascule automatisée

Côté Node.js, la classe BlueGreenDeployment enchaîne le déploiement sur la veille, le canary, le basculement du trafic, puis une fenêtre de surveillance qui déclenche le rollback si le taux d'erreur dépasse le seuil.

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

Le workflow de bascule, étape par étape

Chaque étape porte son propre déclencheur de rollback : si l'une échoue, vous revenez à l'état précédent sans dérouler la suite.

Étape Action Déclencheur de rollback
1 Déployer le nouveau code sur l'environnement en veille Échec du build
2 Lancer les tests canary sur la veille Taux de réussite < 80 %
3 Basculer le trafic vers la nouvelle version
4 Surveiller le taux d'erreur (5 min) Taux d'erreur > 20 %
5 Décommissionner l'ancien environnement

Dépannage

Problème Cause Correctif
Le canary passe mais la production échoue Tâches de test trop simples Rejouez des tâches réelles issues de la file de production
Rollbacks trop fréquents Seuils de surveillance trop agressifs Relevez le seuil d'erreur et allongez la période d'observation
Le trafic n'est pas basculé proprement Requêtes encore en cours sur l'ancien environnement Laissez les tâches en vol se vider avant de décommissionner
Les deux environnements tombent en panne Défaillance d'une dépendance partagée (réseau, API) Ajoutez un disjoncteur ; ne faites pas de rollback pour un incident d'infrastructure

FAQ

Le déploiement bleu-vert double-t-il ma facture CaptchaAI ?

Non. CaptchaAI facture au thread concurrent, pas par environnement : l'environnement en veille ne consomme aucun thread tant qu'il ne résout rien. Vous ne payez que la capacité réellement utilisée pendant la bascule, quel que soit le nombre d'environnements en place.

Comment vider les tâches en cours avant de basculer ?

Cessez d'envoyer de nouvelles tâches à l'ancien environnement, puis attendez que les résolutions déjà lancées se terminent — le polling peut durer jusqu'à quelques minutes. Ne décommissionnez qu'une fois la file en vol vidée, sinon vous perdez des résolutions déjà lancées.

Peut-on faire du bleu-vert sur un seul serveur ?

Oui. Lancez le bleu et le vert comme deux processus ou deux conteneurs sur le même hôte — par exemple une instance chez OVHcloud ou Scaleway — et placez un reverse proxy NGINX devant pour rediriger le trafic d'un port à l'autre.

Bleu-vert ou canary : que choisir pour un pool de workers CAPTCHA ?

Le bleu-vert bascule 100 % du trafic d'un coup et se restaure en une seule opération ; le canary monte le trafic par paliers (1 %, 10 %, 50 %, 100 %). Pour un pool de workers, le bleu-vert est plus simple à opérer ; le canary limite le rayon d'impact sur les très gros volumes.

Prochaines étapes

Déployez en toute confiance : récupérez votre clé API CaptchaAI et mettez en place des mises à jour sans coupure pour vos workers de résolution.

Guides associés :

Les commentaires sont désactivés pour cet article.