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-1ausvariables.tfist selten die richtige Wahl.aws_regionaufeu-central-1und 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 planals Merge-Request-Job,applyals manueller Job auf dem Standard-Branch. - Protokolle und DSGVO: Die CloudWatch-Log-Gruppe aus
ecs.tfsammelt alles, was der Worker ausgibt. Landen dort Page-URLs oder IP-Adressen, wird die Aufbewahrungsfrist zur Frage für Ihre Datenschutzverantwortlichen:retention_in_daysbewusst 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.