DevOps & Scaling

Создание решения CAPTCHA на основе событий с помощью AWS SNS и CaptchaAI

Пока парсер опрашивает res.php в цикле, поток простаивает в ожидании ответа. AWS SNS (Simple Notification Service) убирает этот цикл: CaptchaAI шлёт результат на callback, тот публикует событие в SNS, а с ним независимо работает любое число подписчиков.

Прежде чем настраивать конвейер, проверьте три вещи:

  1. У вас есть AWS-аккаунт с правами на создание тем SNS, функций Lambda и очередей SQS.
  2. API Gateway уже развёрнут и доступен снаружи — CaptchaAI должен дозвониться до вашего callback без VPN и IP-ограничений.
  3. Есть активный API-ключ CaptchaAI и тип CAPTCHA, который вы уже отправляете через in.php/res.php в текущем пайплайне.

Опрос против событийной модели

Подход Что держит поток Когда усложняется
Опрос res.php Поток ждёт, пока не придёт ответ, — простой, но неэффективный На высоком параллелизме растёт число лишних запросов
Callback + SNS Поток свободен сразу после отправки задачи Нужна Lambda и права на публикацию, зато подписчиков можно добавлять без изменений в парсере

Архитектура событийного конвейера

[Scraper] → Submit CAPTCHA → [CaptchaAI API]
                                    ↓
                            Solve completes
                                    ↓
                            Callback → [API Gateway + Lambda]
                                    ↓
                            Publish → [SNS Topic]
                                    ↓
                    ┌───────────────┼───────────────┐
                    ↓               ↓               ↓
            [SQS Queue]      [Lambda Logger]   [Email Alert]
            (result store)   (audit trail)     (on failure)

SNS работает как разветвитель: результат долетает сразу до всех подписчиков, а callback не знает, кто их слушает.

Совет: если получатель результата всего один, отдельная тема SNS — лишний уровень. Начните с прямой обработки в Lambda-callback и переходите на SNS, когда подписчиков станет больше одного.

Шаг 1. Создайте тему AWS SNS

AWS CLI

aws sns create-topic --name captcha-results --output text
# Returns: arn:aws:sns:us-east-1:123456789:captcha-results

Python (boto3)

import boto3

sns = boto3.client("sns", region_name="us-east-1")

response = sns.create_topic(Name="captcha-results")
topic_arn = response["TopicArn"]
print(f"Topic ARN: {topic_arn}")

Шаг 2. Соберите обработчик обратного вызова

Lambda принимает callback CaptchaAI и публикует результат в SNS.

Важно: id и code в query-параметрах приходят от CaptchaAI без подписи — если роут публичный, добавьте проверку по секретному сегменту URL или отдельному токену внутри Lambda, а не полагайтесь на скрытость адреса.

Python (Lambda Handler)

import json
import os
import boto3

sns = boto3.client("sns")
TOPIC_ARN = os.environ["SNS_TOPIC_ARN"]


def lambda_handler(event, context):
    """Receive CaptchaAI callback and publish to SNS."""
    # Parse query parameters from API Gateway
    params = event.get("queryStringParameters", {}) or {}
    task_id = params.get("id", "")
    solution = params.get("code", "")

    if not task_id or not solution:
        return {"statusCode": 400, "body": "Missing id or code"}

    # Publish to SNS
    message = {
        "task_id": task_id,
        "solution": solution,
        "status": "solved"
    }

    sns.publish(
        TopicArn=TOPIC_ARN,
        Message=json.dumps(message),
        Subject="captcha-solved",
        MessageAttributes={
            "task_id": {
                "DataType": "String",
                "StringValue": task_id
            }
        }
    )

    return {"statusCode": 200, "body": "OK"}

Та же логика на Node.js, если callback-Lambda написана на JavaScript:

JavaScript (Lambda Handler)

const { SNSClient, PublishCommand } = require("@aws-sdk/client-sns");

const sns = new SNSClient({ region: "us-east-1" });
const TOPIC_ARN = process.env.SNS_TOPIC_ARN;

exports.handler = async (event) => {
  const params = event.queryStringParameters || {};
  const taskId = params.id;
  const solution = params.code;

  if (!taskId || !solution) {
    return { statusCode: 400, body: "Missing id or code" };
  }

  const message = {
    task_id: taskId,
    solution: solution,
    status: "solved",
  };

  await sns.send(
    new PublishCommand({
      TopicArn: TOPIC_ARN,
      Message: JSON.stringify(message),
      Subject: "captcha-solved",
      MessageAttributes: {
        task_id: { DataType: "String", StringValue: taskId },
      },
    })
  );

  return { statusCode: 200, body: "OK" };
};

Шаг 3. Отправьте CAPTCHA с URL обратного вызова

Укажите в pingback адрес API Gateway — результат придёт сам, без опроса:

Python

import os
import requests

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
CALLBACK_URL = os.environ["CALLBACK_GATEWAY_URL"]  # API Gateway URL


def submit_captcha(sitekey, pageurl):
    """Submit CAPTCHA with SNS-backed callback."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": CALLBACK_URL,
        "json": 1
    })
    data = resp.json()

    if data.get("status") == 1:
        return data["request"]  # task_id
    raise RuntimeError(f"Submit failed: {data.get('request')}")

Шаг 4. Подключите потребителей к теме

К одной теме можно привязать сразу несколько получателей:

  • SQS — очередь, откуда парсер синхронно забирает решения.
  • Lambda — асинхронный обработчик для аудита и метрик, ничего не блокирует.
  • Email — точечные оповещения о сбоях, без нагрузки на основной поток обработки.

Очередь SQS (хранение результатов)

# Subscribe an SQS queue to receive all results
sqs_arn = "arn:aws:sqs:us-east-1:123456789:captcha-results-queue"

sns.subscribe(
    TopicArn=topic_arn,
    Protocol="sqs",
    Endpoint=sqs_arn
)

Lambda (аудит-лог)

# Subscribe a Lambda for audit logging
lambda_arn = "arn:aws:lambda:us-east-1:123456789:function:captcha-audit-logger"

sns.subscribe(
    TopicArn=topic_arn,
    Protocol="lambda",
    Endpoint=lambda_arn
)

Регистратор аудита не блокирует основной поток: если Lambda-логгер упадёт или притормозит, SQS-очередь с результатами всё равно получит своё сообщение независимо от неё.

Email (оповещения о сбоях)

# Subscribe email for error notifications with filter
sns.subscribe(
    TopicArn=topic_arn,
    Protocol="email",
    Endpoint="[email protected]"
)

Шаг 5. Читайте результаты из SQS

Парсер забирает решения из очереди, а не снова стучится в CaptchaAI:

Python

import json
import boto3

sqs = boto3.client("sqs", region_name="us-east-1")
QUEUE_URL = os.environ["SQS_QUEUE_URL"]


def get_solved_captcha(timeout=30):
    """Wait for a CAPTCHA solution from the SQS queue."""
    response = sqs.receive_message(
        QueueUrl=QUEUE_URL,
        MaxNumberOfMessages=1,
        WaitTimeSeconds=min(timeout, 20)  # Long polling (max 20s)
    )

    messages = response.get("Messages", [])
    if not messages:
        return None

    msg = messages[0]
    # SNS wraps the message — unwrap it
    sns_envelope = json.loads(msg["Body"])
    result = json.loads(sns_envelope["Message"])

    # Delete message after processing
    sqs.delete_message(
        QueueUrl=QUEUE_URL,
        ReceiptHandle=msg["ReceiptHandle"]
    )

    return result

Тот же приём на Node.js, если воркер, читающий очередь, написан на JavaScript:

JavaScript

const {
  SQSClient,
  ReceiveMessageCommand,
  DeleteMessageCommand,
} = require("@aws-sdk/client-sqs");

const sqs = new SQSClient({ region: "us-east-1" });
const QUEUE_URL = process.env.SQS_QUEUE_URL;

async function getSolvedCaptcha(timeout = 30) {
  const response = await sqs.send(
    new ReceiveMessageCommand({
      QueueUrl: QUEUE_URL,
      MaxNumberOfMessages: 1,
      WaitTimeSeconds: Math.min(timeout, 20),
    })
  );

  const messages = response.Messages || [];
  if (messages.length === 0) return null;

  const msg = messages[0];
  const snsEnvelope = JSON.parse(msg.Body);
  const result = JSON.parse(snsEnvelope.Message);

  await sqs.send(
    new DeleteMessageCommand({
      QueueUrl: QUEUE_URL,
      ReceiptHandle: msg.ReceiptHandle,
    })
  );

  return result;
}

Фильтрация сообщений AWS SNS

Разным подписчикам нужны разные события — в очередь дежурной команды имеет смысл слать только сбои:

# Only send failures to the ops queue
sns.subscribe(
    TopicArn=topic_arn,
    Protocol="sqs",
    Endpoint=failure_queue_arn,
    Attributes={
        "FilterPolicy": json.dumps({
            "status": ["failed", "error"]
        })
    }
)

Такой фильтр держит очередь дежурной команды чистой: она получает только failed/error, а раздел аналитики может подписаться на весь поток отдельно, не трогая Lambda-callback.

Практический пример развёртывания

Команды с трафиком из России, Беларуси и Казахстана часто размещают тему SNS и Lambda в eu-central-1 — короче сетевой путь до целевых сайтов и меньше промежуточных переходов до callback.

В аудит-логе храните минимум полей — task_id, статус, метку времени — и не добавляйте туда персональные данные заявителя без необходимости. Какой именно состав данных нужен для проверки по 152-ФЗ или GDPR в вашем случае, определяет ваш юрист или DPO; это не заменяет юридическую консультацию, но чем меньше персональных данных в логе, тем проще пройти такую проверку.

Когда не нужен отдельный слой SNS

Пропустите SNS-топик, если:

  • у вас всего один потребитель результата, и он и так вызывается напрямую из callback;
  • объём — единицы задач в минуту, где выигрыш от развязки не окупает лишнюю Lambda;
  • вы уже используете полноценную шину событий (EventBridge, Kafka) и SNS дублировал бы её роль.

Тогда проще обрабатывать callback напрямую и добавить SNS позже, когда подписчиков станет больше одного.

Чек-лист перед продакшеном

  • Callback отвечает 200 без авторизации на роуте, но проверяет подпись или токен внутри самой Lambda.
  • На теме настроено разрешение sns:Publish, и у каждого подписчика есть права на приём сообщений.
  • Включена проверка task_id на стороне потребителей — SNS доставляет минимум один раз, дубли возможны.
  • Задана dead-letter queue (DLQ) для сообщений, которые ни один подписчик не смог обработать.

Типичные проблемы и решения

Проблема Причина Решение
Callback → 403 Auth API Gateway блокирует CaptchaAI Отключите auth, проверяйте токен в Lambda
Нет сообщений в SQS Нет прав sns:Publish Добавьте разрешение в политику очереди
Дубли результатов SNS шлёт минимум один раз Проверяйте task_id перед обработкой
Задержка callback Нет provisioned concurrency Включите его для Lambda

Частые вопросы

Сколько потоков CaptchaAI нужно для SNS-конвейера?

Столько же — тариф задаёт параллелизм, а SNS лишь убирает опрос res.php.

Что делать, если Lambda не получает вызов от CaptchaAI?

Проверьте, что pingback доступен извне и отвечает 200 без авторизации — IAM/Cognito-защита роута его заблокирует.

Можно ли сохранить порядок обработки результатов?

Да, через SNS FIFO и SQS FIFO с MessageGroupId = task_id. Для независимых задач хватит обычной темы.

Нужно ли хранить историю решённых CAPTCHA для аудита?

Решает ваша команда — CaptchaAI отвечает только за решение. Берите минимум полей, задайте срок хранения.

Нужно ли платить AWS отдельно за SNS и SQS?

Да. Тариф CaptchaAI считает только потоки и решённые задачи — инфраструктура AWS (SNS, SQS, Lambda, API Gateway) оплачивается отдельно по вашему AWS-счёту и на небольшом трафике обычно укладывается в бесплатный уровень.

Что дальше

Тема SNS и один подписчик — это уже рабочий конвейер. Дальше подключайте остальных получателей и переходите к смежным руководствам:

Комментарии для этой статьи отключены.