Descrever a frota de workers de CAPTCHA em Terraform resolve dois problemas de uma vez: ninguém mais sobe container na mão e a concorrência vira um número revisado em pull request. O que decide o desenho, porém, não é o tipo de instância — é quantas resoluções podem estar em voo ao mesmo tempo, porque a CaptchaAI cobra por thread simultânea, não por resolução.
O esqueleto abaixo roda em ECS Fargate na AWS: estado remoto, chave no Secrets Manager, definição de tarefa, escalonamento por fila e comandos do dia a dia. O mesmo módulo sobe em dev, staging e produção trocando só o arquivo de variáveis.
Threads do plano definem o teto da frota
Faça esta conta antes do primeiro recurso: as tarefas em voo são worker_count × captchaai_concurrency. Acima das threads contratadas, container extra não vira vazão — vira fila mais longa e conta de computação maior.
| Frota (workers × concorrência) | Tarefas em voo | Plano com threads suficientes |
|---|---|---|
| 1 × 5 | 5 | BASIC (US$ 15/mês, 5 threads) |
| 3 × 5 | 15 | STANDARD (US$ 30/mês, 15 threads) |
| 5 × 10 | 50 | ADVANCE (US$ 90/mês, 50 threads) |
| 5 × 20 | 100 | PREMIUM (US$ 170/mês, 100 threads) |
Todos os planos incluem resoluções ilimitadas por thread no mês, sem cobrança por CAPTCHA. No Terraform você ajusta concorrência, não custo por requisição.
Estrutura do repositório Terraform
O layout mínimo para mais de um ambiente tem duas peças: um módulo reutilizável e um .tfvars por ambiente.
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
O módulo captcha-worker recebe tudo por variável — é o que permite a mesma definição subir com um container em dev e vinte em produção.
Base do projeto: provider, estado e variáveis
Provider AWS e backend remoto com trava
Estado no S3, trava no DynamoDB. Sem essa dupla, dois terraform apply simultâneos — o seu e o do pipeline de CI — corrompem o registro da frota.
# 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
}
Variáveis de entrada
CPU, memória, tamanho da frota e concorrência entram como variáveis. A única que conversa direto com o seu plano é captchaai_concurrency: trate-a como limite contratual, não como parâmetro de tuning.
# 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
}
A chave de API fora do repositório
A chave não entra no .tfvars nem em variável de ambiente em texto puro: fica no Secrets Manager e o ECS a injeta no container em execução. O arquivo de estado, porém, registra em texto claro tudo o que o Terraform lê, inclusive o data source abaixo — daí o encrypt = true no backend.
# 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
}
Cluster ECS Fargate e definição de tarefa
O serviço roda em Fargate com containerInsights ligado. Concorrência e polling entram como variáveis de ambiente comuns; só a chave passa pelo bloco secrets. Os logs vão para o CloudWatch com o prefixo worker, o que separa erro de resolução de erro de infraestrutura.
# 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]
}
}
Escalonamento automático pela profundidade da fila
São duas políticas de step scaling: a frota sobe dois containers quando a fila cresce e desce um por vez quando ela esvazia. O cooldown assimétrico (120 s para subir, 300 s para descer) evita o efeito sanfona. Mantenha max_workers num valor que, multiplicado pela concorrência, caiba nas threads do plano.
# 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
}
}
}
Um arquivo de variáveis por ambiente
Dev existe para validar sintaxe, permissões e o caminho do segredo, não para medir vazão. Um container com concorrência 3 já cumpre o papel.
# environments/dev.tfvars
environment = "dev"
worker_count = 1
max_workers = 3
worker_cpu = 256
worker_memory = 512
captchaai_concurrency = 3
Produção usa cinco containers com concorrência 20: cem tarefas em voo, que pedem um plano com 100 threads.
# environments/production.tfvars
environment = "production"
worker_count = 5
max_workers = 20
worker_cpu = 1024
worker_memory = 2048
captchaai_concurrency = 20
O código que roda dentro do container
O worker é simples: lê a chave do ambiente, trata SIGTERM e faz o ciclo de envio e consulta contra in.php e res.php. Repare no laço de polling — até 60 consultas espaçadas por POLL_INTERVAL — e em CAPCHA_NOT_READY, o único estado que significa "continue esperando".
"""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")
O desligamento gracioso evita descartar trabalho em andamento: o container em drenagem termina a tarefa que pegou antes de sair.
Comandos do Terraform no dia a dia
A sequência não muda entre ambientes; muda o arquivo de variáveis. Rode o plan com o mesmo -var-file do apply que vem depois — boa parte dos incidentes de IaC nasce dessa troca.
# 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
Cenário: uma equipe de QA em São Paulo
Um time brasileiro roda testes de formulário em https://staging.example.com e resolve o reCAPTCHA v2 do próprio ambiente antes de cada release. Dois detalhes locais entram em cena, e ambos viram parâmetro do módulo.
Rede: subir o cluster em sa-east-1 encurta o RTT entre os workers e o resto do ambiente de testes, embora o tempo de resolução dependa do tipo de CAPTCHA, não da região. Retenção de log: se a suíte grava payloads com dados pessoais, defina o retention_in_days do grupo do CloudWatch e trate o prazo como decisão de compliance, alinhada à LGPD (RGPD, em Portugal).
Quando algo dá errado
| Sintoma | Causa provável | O que fazer |
|---|---|---|
| Segredo não encontrado no deploy | Criado, mas ainda sem valor | Preencha o valor antes do terraform apply |
| Container reinicia ao subir | Variável de ambiente ausente ou tag errada | Confira os logs no CloudWatch e a tag no ECR |
| A frota não cresce com a fila cheia | Alarme do CloudWatch ausente | Verifique o ARN do alarme no step scaling |
| Erro de trava de estado | apply anterior interrompido |
Libere com terraform force-unlock <lock-id> |
| Tarefas esperando sem erro | Concorrência acima das threads do plano | Reduza captchaai_concurrency ou suba de plano |
Perguntas frequentes
Quantas threads meu plano precisa ter para a frota do .tfvars?
Multiplique worker_count por captchaai_concurrency e escolha o plano que ofereça esse número de threads ou mais. Cinco containers com concorrência 10 pedem 50 threads — é o ADVANCE (US$ 90/mês, 50 threads).
Como guardo a chave de API sem que ela apareça no arquivo de estado?
Crie o segredo pelo Terraform e preencha o valor fora dele, pela CLI da AWS. Qualquer data source que leia a versão do segredo grava o conteúdo em texto claro no terraform.tfstate. Prefira passar só o ARN para a definição de tarefa.
O escalonamento automático reage sozinho ao volume?
Reage à métrica que você apontar. As políticas de step scaling só disparam com um alarme do CloudWatch associado, normalmente sobre a profundidade da fila. Sem alarme, nada acontece.
Preciso de navegador headless dentro dos workers?
Não para o fluxo de API. O worker envia sitekey e pageurl por HTTP e recebe o token; o navegador só entra quando o teste precisa devolver o token ao formulário. Por isso o container cabe em 512 MB.
Próximas etapas
Comece pequeno: dev com um container e concorrência 3, para validar permissões e o caminho do segredo; depois promova o mesmo módulo trocando o -var-file. Pegue sua chave de API na CaptchaAI e descreva a frota no Terraform.
Guias relacionados: