Si hoy alguien borrara por accidente tu grupo de workers de resolución de CAPTCHA, ¿en cuánto tiempo lo tendrías de vuelta? Con Terraform la respuesta es "un terraform apply". Cluster, roles IAM, secretos, políticas de escalado y variables por entorno viven en archivos versionados en Git, se revisan en un pull request como cualquier otro cambio y se reproducen igual en dev, staging y producción.
Aquí tienes la ruta completa: estructura del repositorio, API key sin texto plano, workers en ECS Fargate, autoescalado y despliegue. HCL y worker en Python listos para copiar.
Por qué declarar los workers en lugar de crearlos a mano
Un worker de CAPTCHA necesita una credencial sensible, una concurrencia que cuadre con los threads contratados y un ciclo de vida irregular: lo levantas para una campaña de scraping, lo apagas al terminar y lo reabres semanas después con otro tamaño. Ahí falla la consola web: nadie recuerda qué variable traía el servicio anterior.
El caso típico en la región: una agencia de datos en Bogotá o Barcelona levanta un entorno para un cliente durante ocho semanas, factura en pesos o euros y paga la API en USD. Con todo declarado, montar el entorno del siguiente cliente es copiar un archivo de variables y cambiar tres números, y un terraform destroy al cerrar deja el entorno a cero, sin nada encendido generando factura. Bajo GDPR/LOPDGDD o la LFPDPPP mexicana, el estado en Git aporta la trazabilidad que pide una auditoría.
Cómo queda organizado el repositorio
Esta estructura separa la raíz del módulo reutilizable y de los valores de cada entorno:
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
La regla práctica: la raíz nunca contiene números mágicos. Todo lo que cambie entre entornos pasa por variables.tf y se rellena desde un .tfvars.
Configuración base: provider y backend remoto
Este archivo fija la versión de Terraform, el provider de AWS y el backend remoto. El estado en S3 con bloqueo en DynamoDB evita que dos personas apliquen a la vez:
# 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
}
Variables de entrada
Fíjate en captchaai_concurrency: son las tareas de CAPTCHA en vuelo por worker, y hay que multiplicarla por el número de workers para no exceder tus threads.
# 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
}
Ese cálculo es lo que más veces se hace mal. CaptchaAI factura por thread concurrente, con resoluciones ilimitadas dentro del mes, no por CAPTCHA resuelto. Con STANDARD ($30/mes, 15 threads) y worker_count = 3, el techo de captchaai_concurrency es 5: el valor por defecto de 10 te dejaría en 30, el doble de lo contratado. Producción —5 workers con concurrencia 20— pide 100 threads: PREMIUM ($170/mes, 100 threads). Más no acelera nada, solo encola tareas.
Gestión de la API key
La clave nunca va en el HCL ni en el archivo de variables. Declara el secreto en AWS Secrets Manager y pásalo a la tarea por referencia, para que el valor no aparezca en el plan ni en el estado:
# 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
}
Crea el valor fuera de Terraform, por consola o CLI, antes del primer apply: declarar el contenedor es cosa del código; conocer su contenido no.
El grupo de workers en ECS Fargate
Observa la separación entre environment —no sensible— y secrets —referencias a Secrets Manager—, más los logs a CloudWatch, tu única ventana cuando algo falla:
# 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]
}
}
Autoescalado según la profundidad de la cola
El pool sube de dos en dos cuando la cola se acumula y baja de una en una cuando queda ocioso; el cooldown más largo en la bajada evita el efecto acordeón:
# 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
}
}
}
Cuidado con el costo: max_workers debe seguir cabiendo en tus threads. Veinte workers con concurrencia 20 piden 400 threads. Ajusta el techo a tu plan, no al revés.
Variables por entorno
Mismo esquema, distintos números. Dev arranca minúsculo para que probar salga casi gratis:
# environments/dev.tfvars
environment = "dev"
worker_count = 1
max_workers = 3
worker_cpu = 256
worker_memory = 512
captchaai_concurrency = 3
Producción usa el doble de CPU y memoria, y un rango de escalado mucho más amplio:
# environments/production.tfvars
environment = "production"
worker_count = 5
max_workers = 20
worker_cpu = 1024
worker_memory = 2048
captchaai_concurrency = 20
El worker que corre dentro del contenedor
La imagen ejecuta este script: lee clave y concurrencia del entorno, atiende SIGTERM para apagarse limpio cuando Fargate reemplaza la tarea, envía el trabajo a in.php y sondea res.php.
"""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")
El bucle usa reCAPTCHA v2 con el método userrecaptcha; el mismo esqueleto sirve para otros tipos cambiando method. El servicio cubre reCAPTCHA v2 y v3, Cloudflare Turnstile y Cloudflare Challenge, GeeTest v3, imagen/OCR y grid, más CaptchaFox (beta), Friendly Captcha (beta) y Lemin (beta); hCaptcha y FunCaptcha no son compatibles y GeeTest v4 figura como próximamente.
El shutdown_handler evita perder tareas a medio resolver: sin él, cada terraform apply que toque la definición de tarea te cuesta las resoluciones en vuelo.
Comandos de despliegue
Del backend inicializado al entorno de pruebas borrado:
# 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
Guarda la salida de terraform plan y revísala en el pull request: exigir aprobación humana en producción separa un cambio de tamaño de un borrado accidental.
Problemas frecuentes y cómo salir de ellos
| Síntoma | Causa habitual | Qué hacer |
|---|---|---|
| Secreto no encontrado al desplegar | Declarado pero sin valor | Crea el valor en Secrets Manager antes del apply |
| Los workers reinician al arrancar | Falta una variable o el tag de imagen es erróneo | Revisa CloudWatch y el tag en ECR |
| El autoescalado nunca se dispara | Alarma inexistente o métrica errónea | Comprueba el ARN de la alarma en la política |
| Error de bloqueo de estado | apply anterior interrumpido |
terraform force-unlock <lock-id> |
| Muchos tiempos de espera agotados | Concurrencia total sobre tus threads | Baja captchaai_concurrency o sube de plan |
Preguntas frecuentes
¿Cuántos threads necesito para el pool que voy a declarar?
Multiplica worker_count por captchaai_concurrency para el piso y repite con max_workers para el techo. Elige el plan que cubra el techo: BASIC ($15/mes, 5 threads), STANDARD ($30/mes, 15), ADVANCE ($90/mes, 50), PREMIUM ($170/mes, 100) y así hasta VIP-3 ($7,500/mes, 5.000).
¿Puedo guardar la API key en un archivo .tfvars?
No. Los .tfvars acaban en Git y el estado guarda en claro cualquier valor que pase por una variable. Usa Secrets Manager y pasa el secreto por referencia.
¿Terraform o Docker Compose para empezar?
Depende de la escala. Con uno o dos workers en una máquina, Docker Compose da resultados en minutos. En cuanto necesites varios entornos o autoescalado, Terraform paga su curva rápido.
¿Cómo evito que un terraform destroy se lleve producción por delante?
Usa backends de estado separados por entorno y exige siempre -var-file explícito. Añadir prevent_destroy al cluster y limitar por IAM quién aplica en producción cierra el hueco.
¿Funciona lo mismo en GCP o Azure?
Sí. Cambia el provider, los recursos —Cloud Run o GKE en GCP, Container Instances o AKS en Azure— y el gestor de secretos. Los módulos y el worker en Python no cambian.
Empieza por el entorno de dev
Obtén tu clave API de CaptchaAI, guárdala en Secrets Manager y aplica el .tfvars de dev con un solo worker. Cuando el primer token vuelva desde res.php, pasar a producción es cambiar tres números.
Guías relacionadas: