So lösen Sie Captchas in AWS Lambda, ohne in ein Timeout zu laufen

Ein Captcha-Lösen in AWS Lambda scheitert an zwei Stellen, und keine davon ist Ihr Code. API Gateway hört nach 29 Sekunden auf, auf die Funktion zu warten, ein reCAPTCHA, das 40 braucht, liefert dem Aufrufer also eine 504, während die Funktion noch arbeitet. Und 127.0.0.1 in der Lambda-Sandbox ist die Sandbox selbst, ein Client, der auf Loopback zeigt, findet dort also nichts, das lauscht. CapSkip läuft auf einem Rechner, der Ihnen gehört, und das ist in diesem Aufbau nie der, auf dem Ihre Funktion läuft. Bringen Sie zuerst die Adresse in Ordnung und holen Sie dann das Lösen vom Anfragepfad herunter.
Was Sie brauchen
- CapSkip, laufend auf einer Windows-Maschine, die Sie kontrollieren. Es ist eine Desktop-Anwendung und läuft nicht in Lambda. Die Funktion ist hier der Client, mehr nicht.
- Eine Lambda-Runtime mit Python 3.10 oder neuer, mit dem CapSkip-Paket im Deployment-Paket oder in einem Layer.
- Der Server-Modus eingeschaltet. Der Local-Modus antwortet auf 127.0.0.1 und nur für dieses Gerät, was einer Funktion in AWS nichts nützt. Der Server-Modus lauscht auf Ihrer Netzwerkadresse oder öffentlichen IP, damit die Funktion ihn über dieselbe API erreichen kann, und beide finden Sie unter Verbindungseinstellungen. Eine statische öffentliche IP ist zu empfehlen, dazu eine Firewall-Regel für die eine Adresse, von der AWS ankommen wird.
- Einen Weg, diese Adresse von der Funktion aus zu erreichen. Schritt 2 behandelt die zwei Varianten, denn eine Funktion, die an ein VPC gehängt ist, verhält sich anders als eine, die es nicht ist.
Warum das 29-Sekunden-Timeout von API Gateway den Entwurf bestimmt
Ein Captcha-Lösen in AWS Lambda muss in drei Grenzen passen. Schreiben Sie sie auf, bevor Sie Code schreiben, denn zusammen schließen sie den naheliegenden Entwurf aus.
| Limit | Wert | Lässt es sich erhöhen? |
|---|---|---|
| Integrations-Timeout von API Gateway | 29 Sekunden als Vorgabe | Bei regionalen und privaten REST-APIs auf Kontingentantrag. AWS warnt, dass die Erhöhung Sie Drosselungskontingent des Kontos kosten kann |
| Timeout der Lambda-Funktion | 3 Sekunden als Vorgabe, höchstens 900 Sekunden bei einer Standardfunktion | Ja, bis zu dieser Obergrenze von 15 Minuten |
| Polling-Timeout von CapSkip | 300 Sekunden für reCAPTCHA, Turnstile und GeeTest, 120 für Bild-Captchas und ALTCHA | Ja, beide sind Optionen im Konstruktor |
Ein synchroner API-Aufruf kann ein langsames reCAPTCHA also nicht abdecken. Die Funktion hat Platz dafür, das Gateway davor nicht, und der Aufrufer sieht eine 504, während das Lösen noch läuft und weiter abgerechnet wird.
Der Workaround, zu dem man als Nächstes greift, ist schlimmer. Früh zurückzukehren und das Lösen in einem Hintergrund-Thread zu beenden, funktioniert nicht, denn nachdem der Handler zurückgekehrt ist, friert Lambda die Ausführungsumgebung ein. AWS sagt es deutlich: Hintergrundprozesse oder Callbacks, die beim Ende der Funktion nicht fertig waren, nehmen ihre Arbeit wieder auf, wenn Lambda die Umgebung wiederverwendet. Wieder aufnehmen, nicht weiterlaufen. Ihr Thread wacht Minuten später auf, mitten im Pollen für ein Captcha, dessen Token längst abgelaufen ist, innerhalb eines Aufrufs, der nichts damit zu tun hat. Nichts wirft einen Fehler. Die Arbeit landet einfach an der falschen Stelle. AWS beschreibt den Lebenszyklus in seinem Leitfaden zur Ausführungsumgebung.
Schritt 1: Das SDK paketieren und die Funktion konfigurieren
Installieren Sie in einen Ordner und zippen Sie ihn mit Ihrem Handler, oder installieren Sie in einen Ordner namens python, zippen Sie diesen und hängen Sie ihn als Layer an. Legen Sie Plattform und Interpreter auf das fest, was die Funktion ausführt, und nicht auf das, was Ihr Laptop ausführt, sonst scheitert der Import beim Cold Start, ohne etwas Brauchbares im Log. Ohne diese Flags löst pip die Wheels für Ihr lokales Python auf, und ein Wheel, das für einen neueren Interpreter gebaut wurde, lädt auf der Runtime nicht.
# 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
Legen Sie danach Timeout und Verbindungsdaten als Konfiguration fest statt im Code, damit dasselbe Paket gegen einen Test-Solver und einen Produktiv-Solver läuft.
# 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}"Setzen Sie das Timeout der Funktion etwas über das eigene Polling-Timeout des Clients, nicht darunter. Darunter beendet Lambda den Aufruf zuerst, und Sie bekommen ein nacktes Task-Timeout in CloudWatch statt der TimeoutException, die Ihnen gesagt hätte, was passiert ist.
Schritt 2: Der Funktion ein NAT gateway und eine Elastic IP geben
Das ist der Schritt, der entscheidet, ob überhaupt etwas funktioniert, und die Antwort hängt an einer Einstellung, die Sie vielleicht nicht als Netzwerkthema gesehen haben.
| Konfiguration der Funktion | Was sie erreichen kann | Was Sie in Ihrer Firewall freigeben |
|---|---|---|
| Nicht an ein VPC gehängt | Das öffentliche Internet, sofort | Nichts Brauchbares. Der Egress kommt von AWS-eigenen Adressen, die wechseln, es lässt sich also keine einzelne IP freigeben |
| An ein VPC gehängt, kein NAT gateway | Nur das, was in diesem VPC liegt. Ihr Solver liegt nicht darin | Nichts. Die Verbindung läuft in ein Timeout, statt abgelehnt zu werden |
| An ein VPC gehängt, über ein NAT gateway geroutet | Das öffentliche Internet, von einer Adresse aus | Die Elastic IP des NAT gateway, und genau diese Form wollen Sie |
Lambda dokumentiert die ersten beiden Zeilen direkt: Funktionen haben standardmäßig Zugang zum öffentlichen Internet, und wer eine an ein VPC hängt, begrenzt sie auf die Ressourcen in diesem VPC, bis die Subnetze der Funktion einen Weg nach draußen haben. Dieser Weg ist ein NAT gateway in einem öffentlichen Subnetz, beschrieben im Leitfaden zum Internetzugang von Lambda. Der Nebeneffekt ist der nützliche Teil. Ein NAT gateway hält eine Elastic IP, jede Lösung kommt also von einer einzigen stabilen Adresse bei Ihrem Rechner an, und Ihre Firewall-Regel kann eine einzige Zeile sein.
Hängen Sie die Funktion an die privaten Subnetze, nicht an das öffentliche. Das ist die Falle, die trotz vorhandenem NAT gateway zu einem Hänger führt: Eine Funktion, die an einem öffentlichen Subnetz hängt, hat keinen Internetzugang, egal was die Routing-Tabelle sagt, die Pakete laufen also einfach ins Leere. Derselbe Leitfaden wiederholt das zweimal.
Ein NAT gateway ist die einfachste Form, die Ihnen eine stabile Quelladresse gibt, nicht die einzige. Ein Site-to-Site VPN oder Direct Connect aus demselben VPC erreicht einen Solver in Ihrem eigenen Netz, ohne dessen Port überhaupt ins Internet zu stellen, und beides lohnt den Aufwand, wenn die Maschine an einem Ort steht, an dem Sie lieber keinen Port öffnen. So oder so: Halten Sie den Port des Solvers für alles andere geschlossen. Der Server-Modus ist weiterhin Ihre Hardware und weiterhin ohne Abrechnung pro Lösung, er ändert nur, wo der Solver lauscht, damit ihn etwas anderes als derselbe Desktop aufrufen kann.
Schritt 3: Das Lösen vom Anfragepfad holen
Angesichts der Grenzen oben gehört ein Captcha-Job in AWS Lambda auf eine Queue und nicht auf die Anfrage. Der Handler, der API Gateway antwortet, sollte nicht der Handler sein, der löst: Nehmen Sie den Job an, legen Sie ihn auf eine Queue und antworten Sie sofort. Eine zweite Funktion liest die Queue und erledigt die Arbeit mit einem Timeout, das zu einem Captcha passt und nicht zu einer Web-Anfrage.
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})}Die Job-ID ist da, damit der Aufrufer später etwas hat, wonach er fragen kann. Legen Sie den Consumer so an, dass er die Arbeit selbst zu Ende bringt, statt einen Token zurückzureichen, denn ein Token, der auf einen zweiten HTTP-Roundtrip wartet, läuft unterwegs meist ab.
Nehmen Sie eine Queue statt eines asynchronen Aufrufs. Lambda wiederholt einen fehlgeschlagenen asynchronen Aufruf standardmäßig zweimal, und ein Captcha-Lösen ist das Falsche, um es blind zu wiederholen: Der zweite Versuch startet mit einem sitekey, dessen Seitenkontext weitergezogen ist, und Sie zahlen für das Lösen so oder so. Eine Queue nimmt die Wiederholungen nicht weg, sie macht sie sichtbar und begrenzt. Sie bekommen einen Visibility Timeout, den Sie steuern, eine Redrive-Policy und eine Dead Letter Queue, in der ein Job, der immer wieder scheitert, an einer Stelle landet, die Sie ansehen können. AWS empfiehlt einen Maximum Receive Count von mindestens fünf, was Raum für einen gedrosselten Versuch lässt, bevor die Nachricht geparkt wird.
Setzen Sie den Visibility Timeout der Queue auf mindestens das Sechsfache des Timeouts der Consumer-Funktion, was AWS aus demselben Drosselungsgrund empfiehlt. Die Reihenfolge ist nicht optional: Lambda prüft das Event Source Mapping und lehnt es ab, wenn das Timeout der Funktion größer ist als der Visibility Timeout. Bei der Funktion mit 330 Sekunden von oben heißt das ein Visibility Timeout von rund 1980 Sekunden.
Vollständiges lauffähiges Beispiel
Der Consumer. Er baut den Client einmal auf, außerhalb des Handlers, damit eine warme Umgebung ihn wiederverwendet, statt sich bei jeder Nachricht neu zu verbinden.
# 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}Melden Sie die fehlgeschlagene Nachricht, statt eine Exception zu werfen. Ein Werfen lässt den ganzen Batch scheitern, und SQS legt dann jede Nachricht darin zurück auf die Queue, auch die, die Sie schon gelöst haben, und genau das ist das Problem doppelter Lösungen, vor dem die Tabelle unten warnt. Eine partielle Batch-Antwort wiederholt nur den Datensatz, der fehlgeschlagen ist, und dafür muss ReportBatchItemFailures am Event Source Mapping gesetzt sein, sonst wird sie nicht beachtet.
Wiederholen Sie bei einer NetworkException und schlucken Sie die anderen drei. Kein Weg zum Solver heißt, der Job kann später gelingen, während ein unlösbares Captcha, ein Timeout oder ein falscher Parameter bei jedem Versuch gleich scheitert und ein erneuter Versuch nur dieselbe Zeit noch einmal verbraucht. Alle vier SDK-Exceptions leiten sich von CapSkipError ab, wenn Sie lieber eine einzige Sache fangen.
Dieses Absenden ist der Sinn des ganzen Entwurfs: Tun Sie das, wofür der Token da ist, im selben Aufruf. Ein reCAPTCHA-Token hält etwa zwei Minuten, wer ihn in eine Datenbank schreibt, damit ein späterer Schritt ihn abholt, holt also meist etwas ab, das bereits abgelaufen ist. Die Einzelheiten zu diesem Zeitfenster stehen im Leitfaden zum reCAPTCHA-v2-Löser, und die rohen Endpunkte hinter jedem SDK-Aufruf sind dokumentiert in der API-Referenz.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Eine 504 von API Gateway nach 29 Sekunden, während CloudWatch die Funktion noch als laufend zeigt | Das Integrations-Timeout, nicht das Timeout der Funktion | Beantworten Sie die Anfrage sofort und lösen Sie auf einer Queue |
| Task timed out after 3.00 seconds | Das voreingestellte Timeout der Funktion, das niemand ändert, bis es zubeißt | Setzen Sie es über das Polling-Timeout des Clients |
| Eine NetworkException, die 127.0.0.1 nennt | Loopback in der Sandbox erreicht die Sandbox, und CapSkip ist nicht darin | Wechseln Sie in den Server-Modus und setzen Sie die Host-Umgebungsvariable |
| Eine Verbindung, die hängt, bis die Funktion in ein Timeout läuft | Die Funktion hängt an einem VPC ohne Weg nach draußen, die Pakete laufen also ins Leere, statt abgelehnt zu werden | Fügen Sie ein NAT gateway hinzu. Die Funktion vom VPC zu lösen stellt den Internetzugang ebenfalls wieder her, aber dann kann Ihre Firewall keine einzelne Adresse freigeben |
| Derselbe Hänger, obwohl ein NAT gateway schon vorhanden ist | Die Funktion hängt am öffentlichen Subnetz statt an den privaten | Hängen Sie sie an die privaten Subnetze, denn das sind die, die auf das NAT gateway geroutet sind |
| Vom Laptop aus funktioniert es, von der Funktion aus nicht | Ihre Adresse zu Hause ist in der Firewall freigegeben, die von AWS nicht | Geben Sie die Elastic IP des NAT gateway frei |
| Jeder Job in einem Batch zweimal gelöst | Ein Datensatz hat geworfen, also hat SQS den ganzen Batch zurückgegeben, auch die Datensätze, die schon erfolgreich waren | Melden Sie den fehlgeschlagenen Datensatz, statt zu werfen, und schalten Sie ReportBatchItemFailures ein |
| Lambda weigert sich, das Event Source Mapping anzulegen | Das Timeout der Funktion ist größer als der Visibility Timeout der Queue, und Lambda prüft das | Setzen Sie den Visibility Timeout auf mindestens das Sechsfache des Funktions-Timeouts |
| Eine Lösung, die während eines fremden Aufrufs fertig wird | Ein Hintergrund-Thread wurde eingefroren, als der Handler zurückkehrte, und beim nächsten Aufruf wieder aufgetaut | Beenden Sie das Lösen, bevor Sie zurückkehren. Fire and forget gibt es hier nicht |
| Eine TimeoutException, die 300 Sekunden nennt | CapSkip hat nicht innerhalb des reCAPTCHA-Polling-Timeouts geantwortet | Prüfen Sie, ob der Solver läuft und nicht ausgelastet ist. Die Obergrenze anzuheben verzögert nur dieselbe Antwort |
| CAPCHA_NOT_READY in einer selbst gebauten Polling-Schleife | Die Antwort ist noch nicht fertig, und das ist ein normaler Zwischenzustand und kein Fehler | Lassen Sie das SDK pollen, oder lesen Sie die Beschreibung in dem Leitfaden zu diesem Code |
| Unable to import module lambda_function, no module named capskip | Das Paket wurde für die falsche Architektur oder den falschen Interpreter installiert, oder es liegt im Layer am falschen Pfad | Installieren Sie mit den Flags für Plattform, Implementierung und Python-Version, und legen Sie den Layer-Inhalt unter python in die Wurzel des Zips |
FAQ
Kann CapSkip selbst in Lambda laufen?
Nein, und das muss es auch nicht. CapSkip ist eine Windows-Anwendung, die auf Hardware läuft, die Ihnen gehört, und das SDK in Ihrer Funktion ist ein schlanker Client dafür über HTTP. Schalten Sie den Server-Modus ein, richten Sie die Funktion auf diese Adresse, und die Funktion ruft es genauso auf wie ein Skript auf demselben Schreibtisch. Das Lösen bleibt auf Ihrem Rechner, und deshalb zählt auch niemand die Zahl der Lösungen ab.
Kann ich hinter API Gateway überhaupt lösen?
Manchmal, und es hängt am Typ und nicht an Ihrer Konfiguration. Ein Bild-Captcha oder ein ALTCHA-Proof-of-Work ist oft in ein, zwei Sekunden fertig, und das passt mit Luft in 29 Sekunden. Ein reCAPTCHA oder eine Turnstile-Challenge-Seite schafft das häufig nicht, und wenn nicht, bekommt der Aufrufer eine 504, während die Arbeit weiterläuft und weiter kostet. Ist das ganze Produkt ein einziger synchroner Endpunkt, beantragen Sie die Erhöhung des Integrations-Timeouts für Ihre REST-API und messen Sie, was Ihr eigener Verkehr tatsächlich braucht. Die Form mit der Queue ist trotzdem die, die Sie um drei Uhr nachts nicht überrascht.
Wie lasse ich nur meine Funktion an den Solver?
Hängen Sie die Funktion an ein VPC, routen Sie ihren ausgehenden Verkehr über ein NAT gateway und geben Sie die Elastic IP dieses Gateways in Ihrer Firewall frei. Das ist die einfachste Form, die Ihnen eine stabile Quelladresse gibt, denn eine Funktion außerhalb eines VPC geht von AWS-eigenen Adressen hinaus, die sich unter Ihnen ändern. Ein Site-to-Site VPN oder Direct Connect erledigt dieselbe Aufgabe, ohne überhaupt einen Port ins Internet zu öffnen. Halten Sie den Port des Solvers für alles andere geschlossen und behandeln Sie den API-Schlüssel als zweites Schloss und nicht als einziges.
Wird die Funktion durch ein langes Lösen teuer?
Lambda rechnet die tatsächlich verstrichene Zeit ab, eine Funktion, die auf eine Antwort wartet, wird also zum gleichen Satz bezahlt wie eine, die rechnet. Das ist ein zweites Argument für die Queue: Der Consumer braucht keine große Speichergröße, denn er wartet auf das Netz, statt zu rechnen, und flussaufwärts wird nichts blockiert, während er wartet. Das Lösen selbst kostet Sie pro Captcha nichts, denn es passiert auf Ihrem eigenen Rechner. Dieselbe Abwägung taucht auf anderen gehosteten Plattformen auf, und der Azure-Functions-Leitfaden spielt das dortige Äquivalent durch.
Die Kurzfassung
Ein Captcha-Lösen in AWS Lambda braucht drei Entscheidungen, und alle fallen, bevor Sie den Handler schreiben. Stellen Sie CapSkip auf den Server-Modus um, denn Loopback in der Lambda-Sandbox erreicht nichts. Hängen Sie die Funktion an ein VPC und routen Sie sie über ein NAT gateway, damit Ihre Firewall eine einzige Elastic IP freigeben kann. Hören Sie dann auf, auf dem Anfragepfad zu lösen: API Gateway gibt Ihnen 29 Sekunden, ein langsames reCAPTCHA braucht mehr, und früh zurückzukehren hilft nicht, weil die Umgebung in dem Moment einfriert, in dem Ihr Handler es tut. Legen Sie den Job auf die Queue, lösen Sie ihn in einem Consumer, dessen Timeout über dem des Clients liegt, und verwenden Sie den Token in demselben Aufruf, der ihn verdient hat.
Jede Methode, die das Python-Paket bereitstellt, samt den Optionen, die sie jeweils annimmt, finden Sie aufgelistet auf der Python-Captcha-Solver-Seite.
Noch ein letzter Punkt zur Wirtschaftlichkeit, denn sie ist es, die den Entwurf mit der Queue bequem macht. Ein verworfener Job kostet Sie die Lambda-Millisekunden und sonst nichts, denn die Captcha-Umgehung läuft auf Hardware, die Sie bereits bezahlt haben, ein erneuter Versuch oder ein weggeworfener abgelaufener Token taucht also nie auf einer Rechnung von irgendjemandem auf.
