Lambda Endlosschleife mit S3: Rekursive Trigger erkennen und dauerhaft verhindern

Eine Lambda-Funktion wird durch einen S3-Upload ausgelöst, verarbeitet die Datei und schreibt das Ergebnis zurück in denselben Bucket — und plötzlich läuft sie im Sekundentakt, die Kosten explodieren, und der CloudWatch-Alarm schweigt, weil niemand einen Schwellenwert für 'Lambda ruft sich selbst auf' konfiguriert hat. Dieses rekursive Trigger-Muster ist einer der häufigsten und teuersten Fehler beim Einstieg in event-getriebene Architekturen mit AWS Lambda und Amazon S3.

TL;DR: Lambda S3 Endlosschleife auf einen Blick

AspektDetails
UrsacheLambda schreibt Output in denselben S3-Bucket/Prefix, der den Trigger auslöst
SymptomExponentiell steigende Invocation-Zahl, AWS-Kosten steigen unkontrolliert
SofortmaßnahmeS3-Event-Notification deaktivieren oder Lambda-Funktion throttlen
Dauerhafte Lösung ASeparater Output-Bucket (empfohlen)
Dauerhafte Lösung BPrefix-Trennung im selben Bucket mit präzisem Filter
AbsicherungCloudWatch Alarm auf Invocation-Anomalien + Concurrency-Limit

Wie der rekursive Lambda-S3-Trigger entsteht

S3-Event-Notifications sind zustandslos und kontextfrei. Wenn ein Objekt in einen Bucket geschrieben wird und für diesen Bucket eine Notification-Konfiguration mit dem Event-Typ s3:ObjectCreated:* existiert, löst S3 den konfigurierten Trigger aus — ohne zu prüfen, wer das Objekt geschrieben hat. Lambda ist für S3 kein besonderer Akteur, sondern ein normaler Schreiber.

Das bedeutet: Sobald die Lambda-Funktion ihr Ergebnis in denselben Bucket schreibt, den sie selbst überwacht, erzeugt sie ein neues ObjectCreated-Event. Dieses Event löst dieselbe Funktion erneut aus. Die Funktion schreibt erneut. Das Muster wiederholt sich, bis ein externes Limit greift — oder bis die AWS-Rechnung die Aufmerksamkeit erzwingt.

graph TD A["Externer Upload
in S3-Bucket"] --> B["S3: ObjectCreated Event"] B --> C["Lambda Invocation #1"] C --> D["Verarbeitung + PutObject
in denselben Bucket"] D --> E["S3: ObjectCreated Event"] E --> F["Lambda Invocation #2"] F --> G["Verarbeitung + PutObject
in denselben Bucket"] G --> H["S3: ObjectCreated Event"] H --> I["Lambda Invocation #3 ..."]; style A fill:#2d6a4f,color:#fff style I fill:#d62828,color:#fff style D fill:#e07a5f,color:#fff style G fill:#e07a5f,color:#fff
  1. Upload: Ein externer Client lädt eine Datei in den S3-Bucket hoch.
  2. Event: S3 erzeugt ein s3:ObjectCreated-Event und ruft Lambda auf.
  3. Verarbeitung: Lambda liest die Datei, verarbeitet sie und schreibt das Ergebnis zurück in denselben Bucket.
  4. Rekursion: Das neue Objekt erzeugt ein weiteres s3:ObjectCreated-Event — die Schleife beginnt.
  5. Eskalation: Jede Iteration kann mehrere Objekte erzeugen, was die Invocation-Rate exponentiell steigert.
Das ist wie ein Mikrofon, das direkt vor seinen eigenen Lautsprecher gehalten wird. Das Signal verstärkt sich selbst, bis jemand den Stecker zieht — oder die Hardware kaputt geht.

Schritt 1: Sofortmaßnahme — Endlosschleife stoppen

Bevor irgendetwas analysiert wird, muss die laufende Schleife unterbrochen werden. Zwei Wege sind sofort wirksam: die Event-Notification deaktivieren oder die Funktion auf Concurrency 0 drosseln. Letzteres ist schneller und reversibel, ohne die Bucket-Konfiguration zu ändern.

Option A: Reserved Concurrency auf 0 setzen (schnellste Bremse)

aws lambda put-function-concurrency \
  --function-name meine-verarbeitungs-funktion \
  --reserved-concurrent-executions 0 \
  --region us-east-1

Lambda lehnt ab diesem Moment alle neuen Invocations mit einem Throttle-Fehler ab. S3 versucht die Zustellung noch einige Male (asynchrone Invocation mit Retry-Verhalten), aber die Schleife stoppt effektiv. Wichtig: Dieser Zustand muss aktiv rückgängig gemacht werden — die Funktion bleibt dauerhaft gedrosselt, bis Concurrency wieder erhöht wird.

Option B: S3-Event-Notification entfernen

aws s3api put-bucket-notification-configuration \
  --bucket mein-bucket \
  --notification-configuration '{}' \
  --region us-east-1

Dies entfernt alle Notification-Konfigurationen des Buckets. Sicherer als Option A, wenn mehrere Funktionen denselben Bucket überwachen und nur eine gestoppt werden soll — dann lieber Option A verwenden, um andere Trigger nicht zu unterbrechen.

Schritt 2: Ursache analysieren — welcher Prefix löst den Trigger aus?

Jetzt, wo die Schleife gestoppt ist, muss verstanden werden, warum der Filter nicht gegriffen hat. Die aktuelle Notification-Konfiguration zeigt, ob ein Prefix-Filter konfiguriert war — oder ob der Trigger auf den gesamten Bucket reagiert.

aws s3api get-bucket-notification-configuration \
  --bucket mein-bucket \
  --region us-east-1

In der Ausgabe ist entscheidend, ob unter LambdaFunctionConfigurations ein Filter mit Key-Regeln (Prefix oder Suffix) definiert ist. Fehlt dieser Block vollständig, reagiert der Trigger auf jedes Objekt im Bucket — unabhängig von Pfad oder Dateiendung.

Gleichzeitig lohnt ein Blick in CloudWatch Logs, um zu verstehen, wie viele Iterationen bereits stattgefunden haben und ob die Funktion bei jedem Aufruf tatsächlich ein neues Objekt geschrieben hat:

aws logs filter-log-events \
  --log-group-name /aws/lambda/meine-verarbeitungs-funktion \
  --filter-pattern 'ERROR' \
  --start-time $(date -d '1 hour ago' +%s000) \
  --region us-east-1

Lambda S3 Endlosschleife dauerhaft verhindern: Zwei Lösungswege

Lösung 1: Separater Output-Bucket (empfohlen)

Die sauberste Lösung ist architektonisch: Input und Output gehören in verschiedene Buckets. Der Trigger überwacht ausschließlich den Input-Bucket, die Funktion schreibt ausschließlich in den Output-Bucket. Eine Rekursion ist strukturell unmöglich.

graph LR Client["Externer Client"] -->|Upload| InputBucket["S3: input-bucket"] InputBucket -->|s3:ObjectCreated| Lambda["Lambda Funktion"] Lambda -->|GetObject| InputBucket Lambda -->|PutObject| OutputBucket["S3: output-bucket"] OutputBucket -.->|Kein Trigger| Lambda style InputBucket fill:#457b9d,color:#fff style OutputBucket fill:#2d6a4f,color:#fff style Lambda fill:#e07a5f,color:#fff
  1. Input-Bucket: Empfängt nur externe Uploads. Kein Lambda-Output landet hier.
  2. Trigger: Event-Notification nur auf dem Input-Bucket konfiguriert.
  3. Lambda: Liest aus Input, schreibt in Output. Keine Schreiboperation auf den Input-Bucket.
  4. Output-Bucket: Empfängt verarbeitete Dateien. Kein Trigger konfiguriert.

Notification-Konfiguration für den Input-Bucket:

aws s3api put-bucket-notification-configuration \
  --bucket mein-input-bucket \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:meine-verarbeitungs-funktion",
        "Events": ["s3:ObjectCreated:*"]
      }
    ]
  }' \
  --region us-east-1

IAM-Berechtigung für die Lambda-Funktion (Least Privilege):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mein-input-bucket/*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mein-output-bucket/*"
    }
  ]
}

Lösung 2: Prefix-Trennung im selben Bucket

Wenn ein zweiter Bucket aus organisatorischen oder Kostengründen nicht in Frage kommt, lässt sich die Rekursion durch strikte Prefix-Trennung verhindern. Der Trigger reagiert nur auf Objekte unter input/, die Funktion schreibt ausschließlich unter output/. Entscheidend: Der Output-Prefix darf niemals mit dem Trigger-Prefix überlappen.

aws s3api put-bucket-notification-configuration \
  --bucket mein-bucket \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:meine-verarbeitungs-funktion",
        "Events": ["s3:ObjectCreated:*"],
        "Filter": {
          "Key": {
            "FilterRules": [
              {
                "Name": "prefix",
                "Value": "input/"
              }
            ]
          }
        }
      }
    ]
  }' \
  --region us-east-1

Im Lambda-Code muss sichergestellt sein, dass der Output-Pfad niemals mit input/ beginnt. Eine defensive Prüfung direkt im Handler verhindert, dass ein Konfigurationsfehler später die Schleife erneut startet:

🔽 Lambda Handler mit Prefix-Schutz (Python, zum Ausklappen)
import boto3
import os

s3 = boto3.client('s3')

INPUT_PREFIX = 'input/'
OUTPUT_PREFIX = 'output/'

def handler(event, context):
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']

        # Schutz: Verhindert Verarbeitung von Output-Objekten,
        # falls der Trigger versehentlich falsch konfiguriert wird
        if not key.startswith(INPUT_PREFIX):
            print(f'Objekt {key} liegt nicht unter {INPUT_PREFIX} — wird übersprungen.')
            return

        # Verarbeitung
        response = s3.get_object(Bucket=bucket, Key=key)
        content = response['Body'].read()
        processed = content.upper()  # Beispielverarbeitung

        # Output-Key explizit unter output/ schreiben
        output_key = OUTPUT_PREFIX + os.path.basename(key)

        # Sicherheitscheck: Output darf nie unter Input-Prefix landen
        if output_key.startswith(INPUT_PREFIX):
            raise ValueError(f'Output-Pfad {output_key} würde Trigger auslösen — Abbruch.')

        s3.put_object(Bucket=bucket, Key=output_key, Body=processed)
        print(f'Verarbeitet: {key} -> {output_key}')

Schritt 3: Absicherung mit CloudWatch Alarm und Concurrency-Limit

Beide Lösungen verhindern die Rekursion strukturell — aber ein Konfigurationsfehler in drei Monaten kann sie erneut einführen. Ein CloudWatch Alarm auf die Invocation-Rate der Funktion gibt frühzeitig Warnung, bevor die Kosten eskalieren. Ergänzend begrenzt ein Reserved Concurrency Limit den maximalen Schaden, falls die Schleife doch wieder entsteht.

CloudWatch Alarm auf Invocation-Anomalie:

aws cloudwatch put-metric-alarm \
  --alarm-name lambda-invocation-anomalie-meine-funktion \
  --metric-name Invocations \
  --namespace AWS/Lambda \
  --dimensions Name=FunctionName,Value=meine-verarbeitungs-funktion \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 2 \
  --threshold 100 \
  --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
  --region us-east-1

Den Schwellenwert (hier 100 Invocations pro Minute) an das erwartete Volumen der Funktion anpassen. Für eine Funktion, die normalerweise 5-10 Mal pro Minute aufgerufen wird, ist 100 ein sinnvoller Anomalie-Indikator.

Reserved Concurrency als Schadensbegrenzung:

aws lambda put-function-concurrency \
  --function-name meine-verarbeitungs-funktion \
  --reserved-concurrent-executions 10 \
  --region us-east-1

Ein Concurrency-Limit von 10 bedeutet: Selbst wenn die Schleife entsteht, kann die Funktion maximal 10 parallele Ausführungen haben. Das begrenzt den Schaden erheblich — auf Kosten von Throttling bei legitimem Traffic-Anstieg. Den Wert entsprechend dem realen Lastprofil wählen.

Erfahrungsbericht: Die stille Falle mit Suffix-Filtern

Ein häufiges Missverständnis: Ein Entwickler konfiguriert einen Suffix-Filter auf .csv, weil die Input-Dateien CSV sind. Die Funktion verarbeitet die Daten und schreibt das Ergebnis als ergebnis.csv zurück in denselben Bucket — ebenfalls mit der Endung .csv. Der Suffix-Filter greift, der Trigger feuert, die Schleife läuft.

Die Diagnose dauert länger als erwartet, weil der Filter 'korrekt konfiguriert' aussieht. Der Fehler liegt nicht im Filter selbst, sondern in der Annahme, dass ein Suffix-Filter allein ausreicht, wenn Input- und Output-Format identisch sind. Suffix-Filter lösen das Problem nur, wenn Output-Dateien eine andere Endung haben als Input-Dateien — oder wenn Lösung 1 (separater Bucket) verwendet wird.

Insight: Ein Filter verhindert Rekursion nur dann zuverlässig, wenn er eine Eigenschaft filtert, die Output-Objekte strukturell nicht besitzen können.

graph TD S1["Symptom: Invocation-Spike"] --> S2["Annahme: Externer Traffic"] S2 --> S3["S3 Access Logs prüfen"] S3 --> S4["Schreiber = Lambda Execution Role"] S4 --> S5["Tatsächliche Ursache:
Output-Suffix = Input-Suffix"] S5 --> S6["Fix: Separater Output-Bucket
oder Prefix-Trennung"] style S1 fill:#d62828,color:#fff style S2 fill:#e07a5f,color:#fff style S4 fill:#457b9d,color:#fff style S6 fill:#2d6a4f,color:#fff
  1. Symptom: Lambda läuft mit hoher Frequenz, CloudWatch zeigt Invocation-Spike.
  2. Erste Annahme: Externer Traffic-Anstieg oder DDoS auf den Bucket.
  3. Warum falsch: S3 Access Logs zeigen als aufrufende Identität die Lambda-Execution-Role, nicht externe Clients.
  4. Tatsächliche Ursache: Lambda schreibt Output mit identischem Suffix zurück in den überwachten Bucket.
  5. Fix: Separater Output-Bucket oder Prefix-Trennung mit defensivem Code-Check.

Concurrency zurücksetzen nach der Sofortmaßnahme

Nach der Implementierung einer dauerhaften Lösung muss die in Schritt 1 gesetzte Concurrency-Begrenzung wieder auf einen sinnvollen Wert gesetzt werden. Concurrency 0 bedeutet, dass die Funktion vollständig gesperrt bleibt.

aws lambda put-function-concurrency \
  --function-name meine-verarbeitungs-funktion \
  --reserved-concurrent-executions 10 \
  --region us-east-1

Oder Reserved Concurrency vollständig entfernen, um wieder auf den Account-weiten Concurrency-Pool zuzugreifen:

aws lambda delete-function-concurrency \
  --function-name meine-verarbeitungs-funktion \
  --region us-east-1

Nächste Schritte und weiterführende Ressourcen zur Lambda S3 Endlosschleife

Die strukturell sicherste Lösung ist immer der separate Output-Bucket — sie macht Rekursion architektonisch unmöglich und vereinfacht IAM-Berechtigungen durch klare Bucket-Grenzen. Prefix-Trennung ist akzeptabel, erfordert aber disziplinierte Code-Reviews, um sicherzustellen, dass Output-Pfade nie in den Trigger-Bereich rutschen.

Ergänzende Maßnahmen für produktive Umgebungen:

  • S3 Access Logs aktivieren, um Schreiboperationen nach Identität auswerten zu können — hilfreich bei der nächsten Diagnose.
  • AWS Cost Anomaly Detection für Lambda und S3 konfigurieren, um kostenbasierte Frühwarnung zu erhalten.
  • Lambda Destinations für asynchrone Invocations nutzen, um fehlgeschlagene Events strukturiert weiterzuleiten.

Offizielle Dokumentation: AWS Lambda mit Amazon S3 verwenden und S3 Event Notification Filtering.

Glossar

BegriffErklärung
S3 Event NotificationMechanismus, über den S3 bei Objektoperationen (z.B. Upload) externe Dienste wie Lambda benachrichtigt.
Reserved ConcurrencyFeste Anzahl gleichzeitiger Lambda-Ausführungen, die exklusiv für eine Funktion reserviert sind. Wert 0 sperrt die Funktion vollständig.
Prefix-FilterKonfiguration in S3-Event-Notifications, die Trigger auf Objekte mit einem bestimmten Schlüsselpräfix (Pfadanfang) beschränkt.
Asynchrone InvocationS3 ruft Lambda asynchron auf — S3 wartet nicht auf die Antwort der Funktion und versucht bei Fehlern automatisch erneut zuzustellen.
Execution RoleIAM-Rolle, unter der eine Lambda-Funktion ausgeführt wird und die ihre Berechtigungen für AWS-API-Aufrufe definiert.

Kommentare

Beliebte Posts aus diesem Blog

EC2 ohne Internetzugang im eigenen VPC – Internet Gateway und Route Table korrekt einrichten

EC2 SSH Verbindungs-Timeout: Security Group Inbound-Regeln richtig konfigurieren