Как решать капчу в AWS Lambda и не ловить таймаут

Решение капчи в AWS Lambda ломается в двух местах, и ни одно из них не связано с вашим кодом. API Gateway перестаёт ждать функцию через 29 секунд, поэтому reCAPTCHA, которой нужно 40, отдаёт вызывающей стороне 504, пока функция ещё работает. А 127.0.0.1 внутри песочницы Lambda указывает на саму песочницу, так что клиент, направленный на loopback, не найдёт ничего слушающего. CapSkip работает на машине, которой владеете вы, а в этой схеме это никогда не та машина, где идёт ваша функция. Сначала почините адрес, а потом уберите решение с пути запроса.
Что понадобится
- CapSkip, запущенный на машине с Windows, которой управляете вы. Это настольное приложение, и внутри Lambda оно не работает. Функция здесь выступает клиентом и не более того.
- Рантайм Lambda с Python 3.10 или новее и пакет CapSkip в пакете развёртывания или в слое.
- Включённый режим Server. Режим Local отвечает на 127.0.0.1 только для этого устройства, а функции, которая работает в AWS, от этого никакого толку. Режим Server слушает ваш сетевой адрес или публичный IP, чтобы функция дотянулась до него по тому же API, и оба задаются в одном месте: Настройки подключения. Рекомендуется статический публичный IP и правило файрвола для того единственного адреса, с которого придёт AWS.
- Способ дотянуться до этого адреса из функции. Шаг 2 разбирает два варианта, потому что функция, подключённая к VPC, ведёт себя иначе, чем неподключённая.
Почему таймаут API Gateway в 29 секунд задаёт всю схему
Решение капчи в AWS Lambda обязано уложиться в три предела. Выпишите их до того, как напишете хоть строчку кода, потому что вместе они отменяют очевидную схему.
| Limit | Значение | Можно ли поднять |
|---|---|---|
| Таймаут интеграции API Gateway | 29 секунд по умолчанию | На региональных и приватных REST API, по запросу квоты. AWS предупреждает, что увеличение может стоить вам квоты троттлинга аккаунта |
| Таймаут функции Lambda | 3 секунды по умолчанию, максимум 900 секунд для обычной функции | Да, вплоть до потолка в 15 минут |
| Таймаут опроса CapSkip | 300 секунд для reCAPTCHA, Turnstile и GeeTest, 120 для графической капчи и ALTCHA | Да, оба задаются опциями конструктора |
Значит, синхронный вызов API не закрывает медленную reCAPTCHA. У функции запас есть, а у шлюза перед ней нет, и вызывающая сторона видит 504, пока решение ещё идёт и ещё оплачивается.
Обходной путь, за который берутся следом, ещё хуже. Вернуться раньше и дорешать в фоновом потоке не получится, потому что после возврата обработчика Lambda замораживает среду выполнения. AWS пишет об этом прямо: фоновые процессы или колбэки, которые не завершились к моменту окончания функции, возобновляются, если Lambda переиспользует среду. Именно возобновляются, а не продолжаются. Ваш поток просыпается минутами позже, посреди опроса капчи, токен которой давно истёк, внутри вызова, к которому он не имеет отношения. Ошибок нет. Работа просто попадает не туда. AWS расписывает жизненный цикл здесь: руководство по среде выполнения.
Шаг 1: упаковываем SDK и настраиваем функцию
Установите пакет в папку и заархивируйте её вместе с обработчиком либо установите его в папку с именем python, заархивируйте её и подключите как слой. Зафиксируйте платформу и интерпретатор под то, что запускает функция, а не под то, что запускает ваш ноутбук, иначе импорт упадёт на холодном старте и в логе не будет ничего полезного. Без этих флагов pip подбирает wheel под ваш локальный Python, а wheel, собранный под более новый интерпретатор, в рантайме не загрузится.
# pip install capskip pip install capskip --target package/ \ --platform manylinux2014_x86_64 --implementation cp \ --python-version 3.12 --only-binary=:all: cp lambda_function.py package/ cd package && zip -r ../function.zip . > /dev/null && cd .. aws lambda update-function-code \ --function-name solve-captcha --zip-file fileb://function.zip
Дальше задайте таймаут и параметры подключения конфигурацией, а не в коде, чтобы один и тот же пакет работал и с тестовым решателем, и с боевым.
# Timeout in seconds, and the Server mode address
aws lambda update-function-configuration \
--function-name solve-captcha \
--timeout 330 \
--environment "Variables={CAPSKIP_HOST=203.0.113.10,CAPSKIP_PORT=8080}"Поставьте таймаут функции чуть выше собственного таймаута опроса клиента, а не ниже. Ниже, и Lambda убьёт вызов первой, а вы получите в CloudWatch голый таймаут задачи вместо TimeoutException, который объяснил бы, что произошло.
Шаг 2: даём функции NAT gateway и Elastic IP
Именно этот шаг решает, заработает ли вообще что-нибудь, и ответ зависит от одной настройки, которую вы могли не считать сетевой.
| Конфигурация функции | Куда она дотянется | Что разрешать на файрволе |
|---|---|---|
| Не подключена к VPC | В публичный интернет, сразу | Ничего полезного. Исходящий трафик идёт с адресов AWS, которые меняются, поэтому один IP в список разрешённых не внесёшь |
| Подключена к VPC, без NAT gateway | Только до того, что внутри этого VPC. Вашего решателя там нет | Ничего. Соединение отваливается по таймауту, а не получает отказ |
| Подключена к VPC, с маршрутом через NAT gateway | В публичный интернет, с одного адреса | Elastic IP этого NAT gateway, и это именно та схема, которая вам нужна |
Первые две строки Lambda описывает прямо: у функций по умолчанию есть доступ в публичный интернет, а подключение функции к VPC ограничивает её ресурсами внутри этого VPC, пока у её подсетей не появится маршрут наружу. Таким маршрутом служит NAT gateway в публичной подсети, и он описан здесь: руководство по доступу Lambda в интернет. Побочный эффект здесь и есть самое полезное. NAT gateway держит Elastic IP, поэтому каждое решение приходит на вашу машину с одного стабильного адреса, а правило файрвола укладывается в одну строку.
Подключайте функцию к приватным подсетям, а не к публичной. Это та самая ловушка, которая даёт зависание при уже поднятом NAT gateway: у функции, подключённой к публичной подсети, доступа в интернет нет, что бы ни говорила таблица маршрутизации, поэтому пакеты просто уходят в никуда. То же руководство повторяет это дважды.
NAT gateway даёт самую простую схему с одним стабильным исходящим адресом, но не единственную. Site-to-Site VPN или Direct Connect из того же VPC дотягиваются до решателя в вашей собственной сети, вообще не выставляя его порт в интернет, и оба стоят настройки, если машина стоит там, где порт открывать не хочется. В любом случае держите порт решателя закрытым для всего остального. Режим Server остаётся вашим железом и остаётся без платы за каждое решение: он лишь меняет то, какой адрес слушает решатель, чтобы вызвать его мог кто-то кроме того же самого рабочего стола.
Шаг 3: убираем решение с пути запроса
С учётом пределов выше задача решения капчи в AWS Lambda принадлежит очереди, а не запросу. Обработчик, который отвечает API Gateway, не должен быть тем же обработчиком, который решает: примите задачу, положите её в очередь и ответьте сразу. Вторая функция читает очередь и делает работу с таймаутом, который подходит капче, а не веб-запросу.
import json, os, uuid, boto3
sqs = boto3.client("sqs")
QUEUE_URL = os.environ["QUEUE_URL"]
def lambda_handler(event, context):
"""API Gateway calls this. It never solves anything."""
body = json.loads(event["body"])
job_id = str(uuid.uuid4())
sqs.send_message(
QueueUrl=QUEUE_URL,
MessageBody=json.dumps({
"job_id": job_id,
"sitekey": body["sitekey"],
"pageurl": body["pageurl"],
}),
)
return {"statusCode": 202,
"body": json.dumps({"job_id": job_id})}Идентификатор задачи нужен, чтобы вызывающей стороне было о чём спросить потом. Проектируйте потребителя так, чтобы он сам доводил работу до конца, а не возвращал token обратно, потому что token, который ждёт второго HTTP-обхода, обычно истекает по дороге.
Берите очередь, а не асинхронный вызов. Lambda по умолчанию повторяет упавший асинхронный вызов дважды, а решение капчи повторять вслепую не стоит: вторая попытка стартует с sitekey, у которого контекст страницы уже ушёл вперёд, а платить за потраченное время придётся в любом случае. Очередь не убирает повторы, она делает их видимыми и ограниченными. Вы получаете visibility timeout, которым управляете сами, политику redrive и dead letter queue, куда падает задача, которая продолжает валиться, и где вы можете на неё посмотреть. AWS рекомендует максимальное число получений не меньше пяти, что оставляет запас на одну попытку, съеденную троттлингом, прежде чем сообщение будет отставлено.
Поставьте visibility timeout очереди не меньше чем в шесть раз больше таймаута функции-потребителя: AWS рекомендует это по той же причине с троттлингом. Порядок здесь не опция: Lambda проверяет event source mapping и отклоняет его, если таймаут функции больше visibility timeout. С функцией на 330 секунд выше это означает visibility timeout около 1980 секунд.
Полный рабочий пример
Потребитель. Он создаёт клиент один раз, вне обработчика, поэтому прогретая среда переиспользует его, а не переподключается на каждом сообщении.
# pip install capskip
import json, os
from urllib.parse import urlencode
from urllib.request import urlopen
from capskip import (CapSkip, ApiException, NetworkException,
TimeoutException, ValidationException)
# Built at cold start and reused while the environment stays warm.
solver = CapSkip(
host=os.environ["CAPSKIP_HOST"], # Server mode address
port=int(os.environ.get("CAPSKIP_PORT", 8080)),
recaptchaTimeout=300,
)
def lambda_handler(event, context):
failures = []
for record in event["Records"]:
job = json.loads(record["body"])
try:
result = solver.recaptcha(
sitekey=job["sitekey"],
url=job["pageurl"],
)
except NetworkException:
# No route to the solver. Retry this one message.
failures.append({"itemIdentifier": record["messageId"]})
continue
except (ApiException, TimeoutException, ValidationException) as exc:
print("giving up on this job:", exc)
continue
# Use the token here. It is short lived, so do not park it.
urlopen(job["pageurl"], data=urlencode(
{"g-recaptcha-response": result["code"]}).encode())
# Needs ReportBatchItemFailures on the event source mapping.
return {"batchItemFailures": failures}Сообщайте об упавшем сообщении, а не бросайте исключение. Брошенное исключение валит весь пакет, и SQS возвращает в очередь каждое сообщение из него, включая уже решённые, а это и есть проблема повторных решений, о которой предупреждает таблица ниже. Частичный ответ по пакету повторяет только упавшую запись, и чтобы он был учтён, на event source mapping нужна настройка report batch item failures.
Повторяйте NetworkException и проглатывайте остальные три. Отсутствие маршрута до решателя означает, что задача может пройти позже, а нерешаемая капча, таймаут или неверный параметр упадут одинаково на каждой попытке, и повтор лишь потратит то же время заново. Все четыре исключения SDK наследуются от CapSkipError, если вам удобнее ловить что-то одно.
Эта отправка и есть смысл всей схемы: сделайте то, ради чего нужен token, внутри того же вызова. Token reCAPTCHA живёт около двух минут, поэтому запись его в базу, чтобы следующий шаг его забрал, обычно означает, что заберут уже истёкший. Подробности про это окно собраны здесь: руководство по сервису распознавания reCAPTCHA v2, а сырые эндпоинты, которые стоят за каждым вызовом SDK, описаны здесь: справочник API.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| 504 от API Gateway через 29 секунд, а в CloudWatch функция всё ещё работает | Таймаут интеграции, а не таймаут функции | Отвечайте на запрос сразу и решайте в очереди |
| Task timed out after 3.00 seconds | Таймаут функции по умолчанию, который никто не меняет, пока он не укусит | Поднимите его выше таймаута опроса клиента |
| NetworkException с указанием 127.0.0.1 | Loopback внутри песочницы ведёт в саму песочницу, а CapSkip там нет | Переключитесь в режим Server и задайте переменную окружения с хостом |
| Соединение висит, пока функция не упадёт по таймауту | Функция подключена к VPC без маршрута наружу, поэтому пакеты уходят в никуда, а не получают отказ | Добавьте NAT gateway. Отключение от VPC тоже вернёт доступ в интернет, но тогда файрвол не сможет разрешить один конкретный адрес |
| То же зависание при уже поднятом NAT gateway | Функция подключена к публичной подсети, а не к приватным | Подключите её к приватным подсетям: именно они смотрят маршрутом на NAT gateway |
| С ноутбука работает, а из функции нет | Ваш домашний адрес файрвол пропускает, а адрес AWS нет | Разрешите Elastic IP этого NAT gateway |
| Каждая задача в пакете решена дважды | Одна запись бросила исключение, поэтому SQS вернул весь пакет, включая записи, которые уже прошли | Сообщайте об упавшей записи вместо исключения и включите report batch item failures |
| Lambda отказывается создать event source mapping | Таймаут функции больше visibility timeout очереди, а Lambda это проверяет | Поднимите visibility timeout минимум до шестикратного таймаута функции |
| Решение завершается во время постороннего вызова | Фоновый поток заморозили при возврате обработчика и разморозили на следующем вызове | Доводите решение до конца до возврата. Отправить и забыть здесь не получится |
| TimeoutException с указанием 300 секунд | CapSkip не ответил в пределах таймаута опроса reCAPTCHA | Проверьте, что решатель запущен и не перегружен. Поднятие потолка лишь отложит тот же самый ответ |
| CAPCHA_NOT_READY в самописном цикле опроса | Ответ ещё не готов, и это нормальное промежуточное состояние, а не ошибка | Доверьте опрос SDK или прочитайте в руководстве по этому коду |
| Unable to import module lambda_function, no module named capskip | Пакет установили под неверную архитектуру или интерпретатор либо он лежит по неверному пути в слое | Устанавливайте с флагами platform, implementation и python version, а содержимое слоя кладите в папку python в корне архива |
FAQ
Может ли сам CapSkip работать внутри Lambda?
Нет, и это не нужно. CapSkip представляет собой приложение для Windows, которое работает на вашем железе, а SDK внутри функции служит для него тонким клиентом поверх HTTP. Включите режим Server, направьте функцию на этот адрес, и она обратится к нему ровно так же, как это сделал бы скрипт на том же столе. Решение остаётся на вашей машине, поэтому число решений никто и не считает.
Можно ли вообще решать капчу за API Gateway?
Иногда, и зависит это от типа, а не от вашей конфигурации. Графическая капча или доказательство работы ALTCHA часто заканчиваются за секунду или две, что укладывается в 29 секунд с запасом. reCAPTCHA или страница-челлендж Turnstile часто не укладываются, и тогда вызывающая сторона получает 504, пока работа продолжается и капает в счёт. Если весь продукт представляет собой один синхронный эндпоинт, запросите увеличение таймаута интеграции для вашего REST API и померяйте, сколько на самом деле занимает ваш собственный трафик. Схема с очередью всё равно остаётся той, которая не удивит вас в три часа ночи.
Как пустить к решателю только мою функцию?
Подключите функцию к VPC, отправьте её исходящий трафик через NAT gateway и разрешите Elastic IP этого шлюза на файрволе. Это самая простая схема с одним стабильным исходящим адресом, потому что функция вне VPC уходит с адресов AWS, которые меняются под вами. Site-to-Site VPN или Direct Connect делают то же самое, вообще не открывая порт в интернет. Держите порт решателя закрытым для всего остального, а API-ключ считайте вторым замком, а не единственным.
Делает ли долгое решение функцию дорогой?
Lambda берёт деньги за фактическую длительность, поэтому функция, которая сидит и ждёт ответа, оплачивается по той же ставке, что и функция, которая считает. Это второй аргумент за очередь: потребителю не нужен большой объём памяти, ведь он ждёт сеть, а не вычисляет, и пока он ждёт, выше по потоку ничто не заблокировано. Само решение не стоит вам ничего за капчу, потому что происходит на вашей машине. Тот же компромисс возникает и на других хостинг-платформах, а руководство по Azure Functions разбирает то же самое применительно к ним.
Коротко
Решение капчи в AWS Lambda требует трёх решений, и все они принимаются до того, как вы напишете обработчик. Переключите CapSkip в режим Server, потому что loopback в песочнице Lambda не ведёт никуда. Подключите функцию к VPC и пустите её через NAT gateway, чтобы файрволу нужно было разрешить один Elastic IP. Дальше перестаньте решать на пути запроса: API Gateway даёт вам 29 секунд, медленной reCAPTCHA нужно больше, а ранний возврат не помогает, потому что среда замирает ровно тогда же, когда замирает ваш обработчик. Ставьте задачу в очередь, решайте её в потребителе, таймаут которого выше клиентского, и используйте token в том же вызове, который его получил.
Все методы, которые предоставляет пакет для Python, вместе с опциями каждого из них перечислены на странице сервиса распознавания капч для Python.
И последнее про экономику, потому что именно она делает схему с очередью комфортной. Выброшенная задача стоит вам миллисекунд Lambda и ничего больше: обход капчи выполняется на железе, за которое вы уже заплатили, поэтому повтор задачи или выброшенный истёкший token никогда не появятся ни в чьём счёте.
