DevOps & Scaling

Penerapan Blue-Green untuk Infrastruktur Pemecahan CAPTCHA

Pertanyaan yang paling sering muncul saat mengganti versi worker penyelesaian CAPTCHA adalah: bagaimana caranya rilis tanpa menjatuhkan pipeline yang sedang berjalan? Jawabannya adalah pola blue-green. Anda menjalankan dua environment kembar — satu melayani traffic langsung (active), satu menganggur sebagai cadangan (standby) — lalu memindahkan seluruh traffic ke versi baru hanya setelah versi itu lolos uji. Jika ada yang rusak, Anda kembali ke environment lama dalam hitungan detik, bukan menit.

Untuk pipeline scraping atau otomatisasi form yang bergantung pada token dari CaptchaAI, downtime beberapa menit saja bisa berarti antrean task menumpuk dan job gagal. Pola ini menghilangkan risiko itu dari persamaan.

Kenapa blue-green cocok untuk worker CAPTCHA

Worker penyelesaian CAPTCHA punya karakter yang membuatnya pas untuk pola ini. Mereka stateless dari sisi bisnis (state sebenarnya ada di CaptchaAI dan di antrean task Anda), sehingga menghidupkan environment kedua tidak memerlukan migrasi data. Yang berubah antar rilis biasanya cuma logika Anda: cara memilih method, penanganan retry, jeda polling, atau versi library HTTP.

Perbedaan inti dengan rolling update: blue-green mengganti 100% traffic sekaligus setelah verifikasi, bukan menukar instance satu per satu. Itu membuat rollback jauh lebih bersih — Anda tinggal menunjuk router kembali ke environment lama yang masih utuh dan sehat.

Arsitektur blue-green

Sebuah traffic router berdiri di depan dua pool worker. Router mengarahkan permintaan solve ke pool active, sementara pool standby memegang versi berikutnya dan hanya menerima traffic uji. Keduanya memanggil endpoint CaptchaAI yang sama.

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

Implementasi router di Python

Router berikut membungkus dua CaptchaWorkerPool. Setiap pool mengirim task ke in.php, melakukan polling ke res.php sampai token siap, dan mencatat statistik keberhasilan sendiri. Method canary_test menjalankan sejumlah task uji di pool standby lebih dulu; peralihan hanya terjadi bila tingkat keberhasilan melewati ambang batas yang Anda tetapkan.

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

Peralihan otomatis dengan Node.js

Versi Node.js membungkus tiga langkah — deploy ke standby, jalankan canary, lalu switch — dalam satu method deploy. Setelah switch, monitorAfterSwitch mengawasi tingkat error selama jendela waktu tertentu dan memicu rollback otomatis jika error melewati 20%. Ini pola yang cocok bila Anda ingin rilis berjalan tanpa operator menunggu di depan terminal.

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

Urutan langkah deployment

Alur di bawah adalah kontrak antara canary dan monitoring. Setiap baris punya pemicu rollback yang eksplisit — jika ambang batasnya terlewati, rilis dibatalkan atau dikembalikan.

Langkah Tindakan Pemicu rollback
1 Deploy kode baru ke environment standby Build gagal
2 Jalankan canary test di standby Tingkat keberhasilan < 80%
3 Alihkan traffic ke versi baru
4 Pantau tingkat error selama 5 menit Tingkat error > 20%
5 Nonaktifkan environment lama

Contoh: rilis dua region Jakarta dan Singapura

Banyak tim di Indonesia men-deploy worker ke ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta) agar latensi ke target tetap rendah. Pola blue-green tetap berjalan mulus di sini: jalankan pool blue dan green di region yang sama, arahkan router lokal ke pool active, dan lakukan canary di standby sebelum switch. Karena penyelesaian CAPTCHA terjadi di sisi CaptchaAI, Anda tidak perlu menggandakan data apa pun antar region — cukup dua set worker yang menunjuk ke API yang sama.

Soal biaya thread: standby yang hanya menerima canary tidak butuh kapasitas penuh. Anda bisa menjalankan pool standby dengan paket kecil seperti BASIC ($15/bulan, 5 thread) untuk uji, lalu menaikkan alokasi thread hanya setelah environment itu jadi active — misalnya ADVANCE ($90/bulan, 50 thread) untuk beban produksi. Karena CaptchaAI menagih per thread konkuren dengan solve tak terbatas per thread, Anda tidak membayar biaya per-CAPTCHA ganda selama masa transisi.

Runbook peralihan traffic

  • Pastikan pool blue dan green lolos health check yang sama sebelum mengirim traffic langsung ke stack baru.
  • Geser traffic dalam tahapan yang eksplisit; pantau latensi dan tingkat penolakan (rejection rate) sampai stabil sebelum melanjutkan ke langkah berikutnya.
  • Segera rollback bila latensi penyelesaian, rejection rate, atau kedalaman antrean melewati ambang batas yang sudah disepakati tim.
  • Tunggu task in-flight di environment lama selesai (drain) sebelum mematikannya, supaya tidak ada token yang hilang di tengah jalan.

Pemecahan masalah

Masalah Penyebab Solusi
Canary lolos tapi produksi gagal Task uji terlalu sederhana Ambil task realistis dari antrean produksi
Rollback terlalu sering terjadi Ambang batas monitoring terlalu agresif Naikkan threshold error; perpanjang masa soak
Traffic tidak bersih terpisah saat switch Ada request in-flight di environment lama Tunggu task in-flight selesai sebelum menonaktifkan
Kedua environment jadi tidak sehat Kegagalan dependensi bersama (jaringan, API) Pakai circuit breaker; jangan rollback untuk masalah infrastruktur

Pertanyaan umum

Apakah saya butuh dua paket CaptchaAI terpisah untuk blue dan green?

Tidak. Satu kunci API sudah cukup untuk kedua environment. Yang menentukan biaya adalah jumlah thread konkuren, bukan berapa environment yang Anda jalankan. Standby yang menganggur nyaris tidak memakai thread; alokasi thread baru terpakai penuh setelah environment itu jadi active.

Bagaimana menentukan ambang batas canary yang tepat?

Mulai dari tingkat keberhasilan minimal 80% pada task uji yang benar-benar mirip produksi, lalu sesuaikan berdasarkan histori. Ambang terlalu tinggi membuat rilis sehat ikut gagal; terlalu rendah meloloskan versi yang cacat. Pantau juga latensi penyelesaian dan tingkat error 5–10 menit pertama setelah switch.

Bisakah blue-green dijalankan di satu server saja?

Bisa. Jalankan blue dan green sebagai proses atau container terpisah di host yang sama, lalu gunakan reverse proxy seperti NGINX untuk mengarahkan traffic antar port. Anda kehilangan isolasi tingkat mesin, tapi mekanisme switch dan rollback-nya tetap sama persis.

Apa bedanya blue-green dengan canary deployment?

Blue-green mengalihkan 100% traffic sekaligus setelah verifikasi. Canary menaikkan porsi traffic ke versi baru secara bertahap (1% → 10% → 50% → 100%). Blue-green lebih sederhana untuk di-rollback; canary lebih halus untuk sistem berskala besar. Keduanya bisa digabung: pakai canary sebagai gerbang uji sebelum switch penuh ala blue-green.

Langkah selanjutnya

Siapkan rilis tanpa downtime dengan percaya diri — ambil kunci API CaptchaAI Anda dan uji versi baru di standby sebelum switch.

Panduan terkait

Komentar dinonaktifkan untuk artikel ini.