DevOps & Scaling

Terraform + CaptchaAI : des workers CAPTCHA en infrastructure as code

Une infrastructure de résolution de CAPTCHA cliquée dans la console AWS n'est reproductible par personne — pas même par celui qui l'a créée. Avec Terraform, le cluster de workers, la clé API et les règles de scaling tiennent dans un dépôt Git : vous relisez un diff avant de toucher la production, vous recréez un environnement de staging à l'identique en quelques minutes, et vous détruisez tout d'une commande une fois la campagne terminée.

Ce guide déroule un module AWS complet — ECS Fargate, Secrets Manager, auto-scaling — branché sur l'API CaptchaAI, avec les .tfvars qui séparent dev, staging et production.

Arborescence du dépôt Terraform

Un module réutilisable, trois jeux de variables : la structure qui tient dans la durée.

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

Le module captcha-worker ignore tout de vos environnements : la différence entre test et production passe uniquement par les variables.

Le socle : provider AWS et état distant

L'état Terraform décrit votre infrastructure entière. Stockez-le dans S3 avec chiffrement et verrouillage DynamoDB dès le premier apply : sans verrou, deux ingénieurs qui lancent un apply en même temps corrompent le fichier d'état.

# 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
}

Pour une équipe européenne, eu-west-3 (Paris) réduit la latence vers vos cibles et garde les logs dans l'UE — un argument facile à défendre en revue RGPD si vos workers journalisent des URL. La minimisation reste votre responsabilité : ne journalisez pas les données personnelles des pages visitées.

Les variables qui pilotent la capacité

# 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
}

captchaai_concurrency est la variable qui coûte de l'argent, pas worker_count : CaptchaAI facture par thread simultané, avec des résolutions illimitées par thread sur le mois — BASIC ($15/mois, 5 threads), ADVANCE ($90/mois, 50 threads), PREMIUM ($170/mois, 100 threads). Règle de dimensionnement : worker_count × captchaai_concurrency ne doit pas dépasser les threads du plan. Cinq workers à 20 tâches demandent 100 threads, soit PREMIUM ; au-delà, CORPORATE ($240/mois, 150 threads) ou ENTERPRISE ($300/mois, 200 threads).

La clé API : dans Secrets Manager, jamais dans le state

Une clé API écrite dans un .tfvars finit en clair dans l'état Terraform, donc dans votre bucket S3 et ses sauvegardes. Créez le secret avec Terraform, renseignez sa valeur ailleurs (console ou CLI), injectez-le par référence.

# 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
}

Le cluster ECS Fargate qui exécute les workers

Fargate évite de gérer des instances : vous décrivez une tâche, ECS trouve où la faire tourner. La définition de tâche ci-dessous passe la concurrence et l'intervalle de polling par variables d'environnement, et la clé API par secrets — jamais par environment.

# 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]
  }
}

À la relecture, vérifiez trois points : sous-réseaux privés (var.private_subnets), logs CloudWatch préfixés, containerInsights activé pour les métriques.

Mise à l'échelle automatique pilotée par la file d'attente

Le signal utile n'est pas le CPU — un worker qui attend une réponse d'API en consomme peu. C'est la profondeur de la file qui révèle le retard : montez vite (cooldown court, deux tâches), redescendez lentement (300 s, une tâche) pour éviter les oscillations.

# 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
    }
  }
}

Le plafond est double : max_workers côté AWS, threads du plan côté CaptchaAI. Doubler les tâches ECS sans threads disponibles allonge seulement vos temps de résolution.

Un fichier .tfvars par environnement

# 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

Dev tourne avec un worker et 3 tâches simultanées — assez pour valider un apply et une intégration reCAPTCHA v2 sans mobiliser le quota. La production monte à 5 workers, 20 tâches chacun. Entre les deux, un palier de staging (2 workers, 5 tâches) suffit au scénario le plus courant : une équipe QA à Lyon qui rejoue chaque nuit un parcours d'inscription protégé par Cloudflare Turnstile.

Le worker qui appelle l'API CaptchaAI

Le conteneur exécute ce script. Il gère SIGTERM proprement — indispensable sur Fargate, où une tâche remplacée reçoit ce signal avant l'arrêt — et interroge res.php jusqu'au token ou à un code d'erreur autre que CAPCHA_NOT_READY.

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

Ici, method=userrecaptcha cible reCAPTCHA v2 ; le token renvoyé s'injecte dans g-recaptcha-response sur la page cible. Pour d'autres types, changez la méthode d'envoi : reCAPTCHA v2 et v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA image/OCR et grilles d'images sont pris en charge. CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) sont disponibles en version bêta. hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge, et GeeTest v4 est annoncé comme à venir : ne construisez pas votre pipeline autour de ces types.

Dérouler le déploiement

# 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

Faites relire la sortie de plan avant chaque apply : c'est là qu'une suppression de service se repère avant la panne. En CI, plan sur chaque pull request, apply après approbation manuelle.

Dépannage

Problème Cause probable Correctif
Secret introuvable au démarrage de la tâche Le secret existe mais sa valeur n'a jamais été renseignée Renseignez la valeur (console ou CLI) avant terraform apply
Les workers redémarrent en boucle Variable d'environnement manquante ou tag d'image ECR erroné Lisez les logs CloudWatch du groupe captcha, vérifiez le tag de l'image
L'auto-scaling ne se déclenche jamais Alarme CloudWatch absente ou métrique mal choisie Vérifiez l'ARN de l'alarme rattachée à la stratégie de scaling
Error acquiring the state lock Un apply précédent a été interrompu Libérez le verrou : terraform force-unlock <lock-id>
Beaucoup de CAPCHA_NOT_READY puis des timeouts Concurrence configurée au-delà des threads du plan Réduisez captchaai_concurrency ou passez au plan supérieur

FAQ

Combien de threads CaptchaAI faut-il pour ma flotte Terraform ?

Multipliez worker_count par captchaai_concurrency : c'est votre besoin en threads simultanés. Trois workers à 10 tâches demandent 30 threads, donc ADVANCE ($90/mois, 50 threads) laisse de la marge. La facturation étant par thread, sur-dimensionner le plan coûte plus cher qu'ajuster la variable.

Faut-il stocker la clé API dans une variable Terraform ?

Non : toute valeur passée en variable se retrouve en clair dans le fichier d'état. Créez la ressource aws_secretsmanager_secret, renseignez sa valeur hors du code, puis référencez l'ARN dans le bloc secrets de la tâche.

Ce module fonctionne-t-il pour hCaptcha ?

Non — hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs). Le module reste valable pour reCAPTCHA v2 et v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3 et les CAPTCHA image/OCR.

Comment adapter la stack à une région européenne ?

Passez aws_region à eu-west-3 dans vos .tfvars, créez le bucket d'état et la table DynamoDB dans la même région, et déplacez le groupe de logs CloudWatch. Le code du worker ne change pas : l'endpoint de l'API CaptchaAI reste le même.

Combien de temps prend un terraform destroy complet ?

Quelques minutes sur un environnement de dev : ECS draine les tâches, puis supprime le service, le cluster et les rôles IAM. Gardez le bucket d'état et le secret hors du module détruit, sinon vous perdez l'historique à chaque cycle.

Prochaines étapes

Codifiez votre infrastructure de résolution, puis mesurez ce qu'elle consomme. Récupérez votre clé API CaptchaAI et lancez votre premier terraform apply sur dev.

Guides associés :

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