DevOps & Skalierung

Terraform + CaptchaAI: Infrastruktur als Code für CAPTCHA-Worker

Eine CAPTCHA-Worker-Flotte gehört in dieselbe Terraform-Konfiguration wie der Rest Ihrer Infrastruktur: ein Modul, eine .tfvars-Datei je Umgebung, ein terraform apply. Dieser Leitfaden zeigt den vollständigen Aufbau – Fargate-Cluster, API-Schlüssel im Secrets Manager, Step-Scaling nach Warteschlangentiefe und den Python-Worker, der die Abfragen an CaptchaAI übermittelt.

Der Unterschied zu einer gewöhnlichen Worker-Flotte steckt in einer einzigen Variablen: captchaai_concurrency. Multipliziert mit der Zahl laufender Worker ergibt sie die Zahl gleichzeitiger Threads – und diese Zahl, nicht das CPU-Budget, entscheidet über die passende CaptchaAI-Stufe.

Was die Konfiguration abdeckt

  • Versionierter Zustand: Remote State in S3, Locking über DynamoDB.
  • Reproduzierbare Umgebungen: dev, staging und production unterscheiden sich nur durch Werte in .tfvars.
  • Kein Schlüssel im Klartext: Der API-Schlüssel liegt im AWS Secrets Manager und erreicht den Container erst zur Laufzeit.
  • Lastabhängige Skalierung: Step-Scaling erhöht die Worker-Zahl, wenn die Warteschlange wächst, und fährt sie im Leerlauf herunter.

Verzeichnisstruktur des Terraform-Repos

Ein Modul, mehrere Umgebungen: Das Worker-Modul kennt keine Umgebungsnamen, es bekommt sie als Variablen übergeben. Dieselbe Definition läuft in dev mit einem Worker und in production mit zwanzig.

terraform/
├── main.tf              # Provider config
├── variables.tf         # Input variables
├── outputs.tf           # Output values
├── modules/
│   └── captcha-worker/
│       ├── main.tf      # ECS/EC2 resources
│       ├── variables.tf # Module inputs
│       └── outputs.tf   # Module outputs
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── production.tfvars

Provider, Remote State und Locking

main.tf pinnt Terraform- und Provider-Version und legt den State nach S3. Das Locking über die DynamoDB-Tabelle ist bei geteilten Umgebungen Pflicht: Ohne Lock überschreibt eine parallel laufende CI-Pipeline den State einer noch offenen Änderung. encrypt = true verschlüsselt die State-Datei im Bucket.

# main.tf
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "captcha-workers/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region
}

Eingabevariablen: Threads sind der eigentliche Hebel

worker_cpu und worker_memory bestimmen, wie viele Verbindungen ein Container offen halten kann. Kostenrelevant ist aber captchaai_concurrency: die Zahl der CAPTCHA-Abfragen, die ein Worker parallel an die API übermittelt. CaptchaAI rechnet Thread-basiert ab – bezahlt werden gleichzeitige Abfragen, nicht einzelne Lösungen. Im Blick behalten müssen Sie deshalb das Produkt aus laufenden Workern und captchaai_concurrency.

# variables.tf
variable "aws_region" {
  description = "AWS region for deployment"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Environment name (dev, staging, production)"
  type        = string
}

variable "worker_count" {
  description = "Number of CAPTCHA solving workers"
  type        = number
  default     = 3
}

variable "worker_cpu" {
  description = "CPU units for each worker (1024 = 1 vCPU)"
  type        = number
  default     = 512
}

variable "worker_memory" {
  description = "Memory in MB for each worker"
  type        = number
  default     = 1024
}

variable "max_workers" {
  description = "Maximum workers for auto-scaling"
  type        = number
  default     = 10
}

variable "captchaai_concurrency" {
  description = "Concurrent CAPTCHA tasks per worker"
  type        = number
  default     = 10
}

API-Schlüssel: Secrets Manager statt Klartext

Der Schlüssel gehört weder in eine .tfvars-Datei noch in eine gewöhnliche Umgebungsvariable der Task-Definition: Beides landet im Klartext im Terraform-State. Terraform legt hier nur die Hülle des Secrets an; den Wert schreiben Sie einmalig über die AWS CLI hinein. Die ECS-Task referenziert ihn über den ARN.

# secrets.tf — Store API key in AWS Secrets Manager
resource "aws_secretsmanager_secret" "captchaai_api_key" {
  name        = "${var.environment}/captchaai-api-key"
  description = "CaptchaAI API key for CAPTCHA solving workers"
}

# Reference secret in ECS task (never in plain text)
data "aws_secretsmanager_secret_version" "captchaai_api_key" {
  secret_id = aws_secretsmanager_secret.captchaai_api_key.id
}

Fargate-Cluster für die Worker

ecs.tf legt Cluster, Task-Definition und Service an. Zwei Details lohnen den zweiten Blick: Die Task-Definition trennt unkritische Werte (environment) sauber vom API-Schlüssel (secrets), und der Service läuft in privaten Subnetzen – die Worker brauchen ausgehendes HTTPS zu ocr.captchaai.com, aber keine öffentliche IP.

# ecs.tf — Fargate-based CAPTCHA workers
resource "aws_ecs_cluster" "captcha" {
  name = "captcha-workers-${var.environment}"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_task_definition" "captcha_worker" {
  family                   = "captcha-worker-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.worker_cpu
  memory                   = var.worker_memory
  execution_role_arn       = aws_iam_role.ecs_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "captcha-worker"
      image = "${aws_ecr_repository.captcha_worker.repository_url}:latest"

      environment = [
        { name = "CAPTCHAAI_CONCURRENCY", value = tostring(var.captchaai_concurrency) },
        { name = "CAPTCHAAI_POLL_INTERVAL", value = "5" },
        { name = "ENVIRONMENT", value = var.environment },
      ]

      secrets = [
        {
          name      = "CAPTCHAAI_API_KEY"
          valueFrom = aws_secretsmanager_secret.captchaai_api_key.arn
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.captcha.name
          "awslogs-region"        = var.aws_region
          "awslogs-stream-prefix" = "worker"
        }
      }
    }
  ])
}

resource "aws_ecs_service" "captcha_worker" {
  name            = "captcha-workers"
  cluster         = aws_ecs_cluster.captcha.id
  task_definition = aws_ecs_task_definition.captcha_worker.arn
  desired_count   = var.worker_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = var.private_subnets
    security_groups = [aws_security_group.captcha_worker.id]
  }
}

Skalierung nach Warteschlangentiefe

Zwei Step-Scaling-Policies genügen: hoch in Zweierschritten mit 120 Sekunden Cooldown, herunter in Einerschritten mit 300 Sekunden. Die Asymmetrie ist Absicht – Spitzen schnell auffangen, Kapazität langsam abbauen. Die Policies allein skalieren allerdings nichts: Sie brauchen einen CloudWatch-Alarm auf der Warteschlangentiefe als Auslöser. Metrik und Schwellenwerte behandelt der Leitfaden zum automatischen Skalieren von CAPTCHA-Workern.

# autoscaling.tf
resource "aws_appautoscaling_target" "captcha" {
  max_capacity       = var.max_workers
  min_capacity       = var.worker_count
  resource_id        = "service/${aws_ecs_cluster.captcha.name}/${aws_ecs_service.captcha_worker.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

# Scale up when queue is deep
resource "aws_appautoscaling_policy" "scale_up" {
  name               = "captcha-scale-up"
  policy_type        = "StepScaling"
  resource_id        = aws_appautoscaling_target.captcha.resource_id
  scalable_dimension = aws_appautoscaling_target.captcha.scalable_dimension
  service_namespace  = aws_appautoscaling_target.captcha.service_namespace

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 120

    step_adjustment {
      scaling_adjustment          = 2
      metric_interval_lower_bound = 0
    }
  }
}

# Scale down when idle
resource "aws_appautoscaling_policy" "scale_down" {
  name               = "captcha-scale-down"
  policy_type        = "StepScaling"
  resource_id        = aws_appautoscaling_target.captcha.resource_id
  scalable_dimension = aws_appautoscaling_target.captcha.scalable_dimension
  service_namespace  = aws_appautoscaling_target.captcha.service_namespace

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 300

    step_adjustment {
      scaling_adjustment          = -1
      metric_interval_upper_bound = 0
    }
  }
}

Eine .tfvars-Datei je Umgebung

dev fährt mit einem Worker und drei parallelen Abfragen, production startet mit fünf Workern zu je zwanzig. Eine weitere Umgebung entsteht durch Kopieren der Datei – nicht durch Ändern des Moduls.

# environments/dev.tfvars
environment           = "dev"
worker_count          = 1
max_workers           = 3
worker_cpu            = 256
worker_memory         = 512
captchaai_concurrency = 3
# environments/production.tfvars
environment           = "production"
worker_count          = 5
max_workers           = 20
worker_cpu            = 1024
worker_memory         = 2048
captchaai_concurrency = 20

Der Worker-Container: übermitteln, abfragen, sauber beenden

Der Container erledigt drei Aufgaben: Er übermittelt die Aufgabe an in.php (Methode userrecaptcha, dazu googlekey und pageurl), fragt res.php im Fünf-Sekunden-Takt ab, bis status auf 1 springt, und behandelt SIGTERM als geordnetes Stoppsignal. Der letzte Punkt ist unter Fargate entscheidend: Beim Herunterskalieren bekommt der Container SIGTERM und bricht ohne Handler mitten in einer laufenden Abfrage ab.

Die Schleife läuft maximal 60 Durchgänge à 5 Sekunden, bevor sie mit TIMEOUT endet – reichlich Reserve, denn reCAPTCHA v2 wird typischerweise in unter 60 Sekunden gelöst. Jede Antwort außer CAPCHA_NOT_READY ist ein echter Fehler und beendet die Schleife sofort; die möglichen Werte listen die Antwortformate und Fehlercodes der API.

"""captcha_worker.py — The container runs this."""
import os
import time
import signal
import requests

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
CONCURRENCY = int(os.environ.get("CAPTCHAAI_CONCURRENCY", "10"))
POLL_INTERVAL = int(os.environ.get("CAPTCHAAI_POLL_INTERVAL", "5"))

running = True

def shutdown_handler(signum, frame):
    global running
    print("Graceful shutdown initiated")
    running = False

signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

session = requests.Session()

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

    captcha_id = data["request"]
    for _ in range(60):
        time.sleep(POLL_INTERVAL)
        result = 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:
            return {"solution": result["request"]}
        if result.get("request") != "CAPCHA_NOT_READY":
            return {"error": result.get("request")}

    return {"error": "TIMEOUT"}

# Main loop — pull tasks from SQS or Redis
print(f"Worker started: concurrency={CONCURRENCY}")
while running:
    # Pull tasks from your queue here
    time.sleep(1)

print("Worker shutdown complete")

Rollout: init, plan, apply

Die Reihenfolge bleibt in jeder Umgebung dieselbe: terraform init holt Provider und Backend, terraform plan mit der passenden .tfvars-Datei zeigt das Delta, terraform apply setzt es um – produktiv erst nach manueller Freigabe.

# Initialize
terraform init

# Plan for production
terraform plan -var-file=environments/production.tfvars

# Apply
terraform apply -var-file=environments/production.tfvars

# Destroy (dev cleanup)
terraform destroy -var-file=environments/dev.tfvars

Thread-Budget und Tarifstufe aufeinander abstimmen

Rechnen Sie vor dem ersten apply aus, wie viele Threads jede Umgebung anfordert: gleichzeitige Threads sind aktive Worker × captchaai_concurrency.

Umgebung aktive Worker captchaai_concurrency gleichzeitige Threads passende Stufe
dev, Grundlast 1 3 3 BASIC (15 $/Monat, 5 Threads)
staging 2 10 20 ADVANCE (90 $/Monat, 50 Threads)
production, Grundlast 5 20 100 PREMIUM (170 $/Monat, 100 Threads)
production, voll skaliert 20 20 400 VIP-1 (1.500 $/Monat, 1.000 Threads)

Die letzte Zeile ist der klassische Stolperstein: max_workers = 20 erlaubt der Flotte, ein Vielfaches dessen anzufordern, wofür die Grundlast ausgelegt wurde. Buchen Sie deshalb nach max_workers × captchaai_concurrency – oder senken Sie max_workers. Preise in US-Dollar.

Terraform-Betrieb im DACH-Umfeld

  • Region bewusst setzen: Der Default us-east-1 aus variables.tf ist selten die richtige Wahl. aws_region auf eu-central-1 und der State-Bucket in derselben Region sparen Latenz.
  • GitLab CI statt GitHub Actions: In vielen deutschen Unternehmen läuft die Pipeline auf einer selbst gehosteten GitLab-Instanz – terraform plan als Merge-Request-Job, apply als manueller Job auf dem Standard-Branch.
  • Protokolle und DSGVO: Die CloudWatch-Log-Gruppe aus ecs.tf sammelt alles, was der Worker ausgibt. Landen dort Page-URLs oder IP-Adressen, wird die Aufbewahrungsfrist zur Frage für Ihre Datenschutzverantwortlichen: retention_in_days bewusst setzen, keine vollständigen Anfragedaten protokollieren.

Bei konstanter Grundlast kann ein fester Server bei Hetzner oder netcup kostengünstiger sein als Fargate; am Terraform-Aufbau ändert das wenig. Wer lieber konfigurationsgetrieben arbeitet, findet den Ablauf unter Worker-Deployment mit Ansible.

Fehlerbehebung

Symptom Ursache Vorgehen
apply bricht mit fehlendem Secret ab Hülle existiert, Wert wurde nie geschrieben Wert einmalig über die AWS CLI setzen, dann apply wiederholen
Flotte skaliert nicht hoch Step-Scaling-Policy ohne auslösenden CloudWatch-Alarm Alarm-ARN und Metrik der Warteschlange prüfen, Schwellenwert testweise senken
Error acquiring the state lock Vorheriger Apply wurde abgebrochen Lock-ID aus der Meldung übernehmen, terraform force-unlock ausführen
Häufige TIMEOUT-Ergebnisse trotz freier Container Mehr gleichzeitige Abfragen als gebuchte Threads max_workers × captchaai_concurrency nachrechnen und eine der Größen anpassen

Häufige Fragen

Wie viele Threads braucht mein Terraform-Deployment?

So viele, wie die Flotte in der Spitze anfordert: max_workers × captchaai_concurrency. worker_count ist nur die Untergrenze. Wer mit der Grundlast kalkuliert, staut bei jeder Skalierung Abfragen an.

Gehört der CaptchaAI-API-Schlüssel in die tfvars-Datei?

Nein. Alles, was Terraform als Variablenwert sieht, steht im Klartext im State. Legen Sie den Schlüssel im AWS Secrets Manager ab und referenzieren Sie ihn – wie in secrets.tf – über den ARN.

Was passiert bei terraform destroy mit laufenden Abfragen?

ECS schickt jeder Task ein SIGTERM; der Worker fängt es ab und beendet seine Schleife geordnet. Ergebnisse bereits übermittelter Aufgaben holt danach niemand mehr ab – leeren Sie die Warteschlange, bevor Sie eine produktive Umgebung abbauen.

Funktioniert dieselbe Konfiguration für Turnstile oder GeeTest v3?

Ja, ohne Änderung an der Infrastruktur. Getauscht wird nur der method-Parameter im Worker: turnstile, geetest oder post für Bild- und Rasterbild-CAPTCHAs. Die Thread-Rechnung bleibt identisch, nur der Durchsatz je Thread verschiebt sich – Turnstile wird typischerweise in unter 10 Sekunden gelöst.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.