Пул воркеров, которые решают CAPTCHA, описывается в Terraform целиком: контейнер, число задач, политики масштабирования и API-ключ CaptchaAI в менеджере секретов. Дальше окружение поднимается одной командой terraform apply, а после нагрузочного прогона так же аккуратно сносится terraform destroy — без забытых ECS-сервисов, которые ещё месяц тикают в счёте.
Ручная сборка через консоль AWS выглядит быстрее ровно один раз. На втором окружении никто уже не помнит, почему в dev таймаут опроса 5 секунд, а в staging — 15. Код в git отвечает на такой вопрос сразу, скриншот консоли — никогда.
Что даёт описание CAPTCHA-инфраструктуры в Terraform
Три вещи, ради которых стоит переносить пул воркеров в код:
- Воспроизводимость. Тот же
.tf-файл и тот же.tfvarsдают одинаковое окружение в другом регионе или аккаунте — dev в европейском регионе и прод ближе к целевым сайтам перестают расходиться конфигурациями. - Ревью изменений.
terraform planв pull request показывает, что именно поменяется: число задач, лимит памяти, политика масштабирования. Это обычное код-ревью, а не переписка в чате. - Контроль расходов. Ночной парсинг закончился —
terraform destroyна dev-окружении, и платить не за что.
Раскладка каталогов
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
Модуль captcha-worker держит всё, что относится к пулу решения CAPTCHA; корневые файлы отвечают только за провайдера и передачу переменных. Так тот же модуль подключается во второй регион правкой одного блока.
Провайдер и удалённое состояние
Начните с бэкенда. Локальный terraform.tfstate на ноутбуке инженера — частая причина расхождений: двое применяют изменения параллельно и затирают работу друг друга. S3 плюс таблица блокировок в DynamoDB закрывают это.
# 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
}
Переменные модуля
Все параметры пула вынесены в переменные, чтобы разница между окружениями сводилась к одному .tfvars-файлу. Обратите внимание на captchaai_concurrency: это число одновременных задач CAPTCHA на воркер, и оно должно сходиться с числом потоков тарифа — расчёт ниже.
# 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-ключ CaptchaAI: только через менеджер секретов
Ключ не попадает ни в .tf-файлы, ни в git. Terraform создаёт запись в AWS Secrets Manager, а ECS подставляет значение в контейнер на старте.
# 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
}
Значение секрета заполняется отдельно — вручную или через CI, но не в Terraform-коде: иначе ключ окажется в state-файле открытым текстом. Заодно проверьте, что уходит в CloudWatch: собирайте только те данные, которые вы вправе обрабатывать (ориентир — 152-ФЗ «О персональных данных», для трансграничных сценариев — GDPR). API-ключ в логах — типичная утечка на ровном месте.
Кластер ECS Fargate
Fargate избавляет от управления инстансами: вы описываете задачу, ёмкость приходит сама. Для решения CAPTCHA это удобно — очередь то пустая, то на несколько тысяч задач.
# 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]
}
}
Без включённого containerInsights политики масштабирования не на что опереть.
Автомасштабирование по глубине очереди
Масштабировать такие воркеры по CPU бесполезно: контейнер почти всё время ждёт ответа внешнего API и по процессору простаивает. Правильный сигнал — длина очереди задач.
# 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
}
}
}
Разные значения cooldown не случайны: вверх пул растёт быстро (120 с), вниз сжимается медленно (300 с) — чтобы не отпускать воркеры на короткой паузе в очереди.
Переменные для каждой среды
Dev-окружение держит один воркер и три параллельные задачи — этого достаточно, чтобы проверить интеграцию:
# 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
Сколько потоков заложить в тариф
captchaai_concurrency — это не абстрактное число, а ваш договор с тарифом. CaptchaAI тарифицируется по одновременным потокам, а не за каждое решение; в пределах месяца число решений на поток не ограничено. Один поток — одна задача CAPTCHA «в полёте»; как только она завершилась, поток берёт следующую.
Простая арифметика для прод-конфигурации выше: 5 воркеров × 20 одновременных задач = 100 потоков в пике. Это ровно тариф PREMIUM ($170/мес, 100 потоков). Уменьшите captchaai_concurrency до 10 — и хватит ADVANCE ($90/мес, 50 потоков). Для команды, которая только начинает и гоняет интеграционные тесты в staging, стартовая точка — BASIC ($15/мес, 5 потоков), то есть один воркер с captchaai_concurrency = 5.
Для агентств и фрилансеров, которые выставляют счета в национальной валюте, а инфраструктуру оплачивают в долларах, фиксированный месячный платёж планируется проще оплаты за каждое решение. Потолок max_workers выставляйте с оглядкой на купленные потоки: иначе автомасштабирование поднимет задачи, которым потоков не хватит, и они будут ждать в очереди.
Код воркера
Контейнер запускает вот это. Логика простая: отправить задачу в in.php, получить ID, опрашивать res.php до готовности токена. Отдельно реализован корректный останов по SIGTERM — ECS присылает его при снижении числа задач, и воркер должен успеть дописать текущее решение, а не оборваться на середине.
"""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")
Пример выше отправляет userrecaptcha для reCAPTCHA v2 — токен приходит в поле g-recaptcha-response. Для других типов меняется только параметр method и набор полей: разбор по типам есть в руководствах по reCAPTCHA v2, Cloudflare Turnstile и GeeTest v3. Учтите ограничения матрицы поддержки: hCaptcha и FunCaptcha сервис не решает, GeeTest v4 заявлен как «скоро», а CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) доступны в бета-режиме.
Команды развёртывания
# 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
Правило для команды: terraform plan с -var-file окружения обязателен перед каждым apply, а вывод плана прикладывается к pull request.
Разбор типичных ошибок
| Симптом | Причина | Что сделать |
|---|---|---|
Secrets Manager can't find the specified secret при деплое |
Запись секрета создана, но значение не заполнено | Записать значение ключа до terraform apply |
| Задачи падают сразу после старта | Нет переменных окружения или неверный тег образа | Смотреть логи в CloudWatch, сверить тег в ECR |
| Автомасштабирование не срабатывает | Нет CloudWatch-алерта или выбрана не та метрика | Проверить ARN алерта в политике масштабирования |
Error acquiring the state lock |
Предыдущий apply прервали |
Снять блокировку: terraform force-unlock <lock-id> |
| Задачи стоят в очереди, воркеры простаивают | Потоков тарифа меньше, чем суммарный captchaai_concurrency |
Снизить параллелизм или перейти на тариф выше |
Частые вопросы
Fargate или EC2 для воркеров решения CAPTCHA?
Fargate — когда нагрузка рваная: платите за время работы задачи и не следите за инстансами. EC2 с зарезервированными инстансами выгоднее при ровном круглосуточном потоке. Практичный путь — начать на Fargate и перенести на EC2 только стабильно загруженный пул.
Как хранить состояние Terraform, если над проектом работают несколько инженеров?
Только удалённый бэкенд с блокировкой: S3 плюс DynamoDB, как выше. Для каждого окружения — свой ключ состояния или workspace. Локальный state-файл в такой команде рано или поздно приведёт к затиранию чужих изменений.
Почему API-ключ нельзя просто положить в переменную Terraform?
Любое значение из конфигурации попадает в state-файл открытым текстом, даже если ресурс помечен как чувствительный. State лежит в S3, и доступ к бакету становится доступом к ключу. Terraform создаёт только запись в Secrets Manager; значение пишется отдельно — вручную или из CI.
Как понять, сколько потоков CaptchaAI нужно купить?
Посчитайте пиковое число одновременных задач: число воркеров × captchaai_concurrency. Полученное значение и есть требуемое число потоков. Если пик редкий и короткий, дешевле оставить тариф ниже и позволить очереди подождать — задачи не теряются, растёт только время ожидания.
Что делать, если воркер получает CAPCHA_NOT_READY слишком долго?
Это нормальный ответ: решение ещё не готово. Проблема появляется, когда цикл опроса упирается в лимит попыток. Увеличьте число итераций или интервал POLL_INTERVAL, а заодно проверьте, что таймаут задачи ECS больше суммарного времени опроса — иначе контейнер остановят раньше, чем придёт токен.
Что дальше
Поднимите dev-окружение на одном воркере, убедитесь, что токен приходит, и только потом включайте автомасштабирование.