Boucle Infinie Lambda avec S3 : Diagnostiquer et Stopper une Récursion Incontrôlée

Votre fonction Lambda se déclenche sur un upload S3, traite le fichier, puis écrit le résultat dans le même bucket — et soudain, les invocations s'enchaînent sans fin, la facture grimpe, et CloudWatch déborde de logs. C'est l'un des pièges les plus classiques de l'architecture event-driven sur AWS, et il suffit d'un préfixe mal configuré pour transformer un pipeline innocent en boucle récursive qui consomme tout votre quota de concurrence.

TL;DR — Boucle Infinie Lambda S3

ProblèmeCauseSolution
Lambda se déclenche en boucleNotification S3 sans filtre de préfixe/suffixeSéparer les préfixes source/destination ou utiliser des buckets distincts
Concurrence épuiséeInvocations exponentiellesPoser un reserved concurrency limit immédiatement
Coûts incontrôlésMillions d'invocations en quelques minutesActiver une alarme CloudWatch sur Invocations + throttle
Difficile à détecterChaque invocation semble légitimeInspecter les préfixes des objets déclencheurs dans les logs

Comment Fonctionne le Déclenchement S3 → Lambda

Avant de corriger, il faut comprendre exactement ce qui se passe. S3 Event Notifications envoie un événement à Lambda chaque fois qu'un objet correspond aux critères configurés sur le bucket. Ce mécanisme est asynchrone : S3 publie l'événement, Lambda le consomme depuis une file interne gérée par AWS. Il n'y a pas de déduplication native — si votre fonction écrit un objet qui correspond au même filtre que celui qui l'a déclenchée, un nouvel événement est émis immédiatement.

graph TD A["Upload initial
input/fichier.csv"] --> B["S3 Event Notification
s3:ObjectCreated:*"] B --> C["Lambda invoquée
Traitement du fichier"] C --> D["Écriture S3
output/fichier_processed.csv"] D --> E["Nouvelle notification S3
même trigger, pas de filtre"] E --> F["Lambda invoquée à nouveau"] F --> G["Écriture S3 #2"] G --> H["Notification S3 #3"] H --> I["... boucle infinie"] style A fill:#2d6a4f,color:#fff style I fill:#c1121f,color:#fff style E fill:#e76f51,color:#fff style H fill:#e76f51,color:#fff
  1. Upload initial : un objet arrive dans s3://mon-bucket/input/fichier.csv.
  2. Notification S3 : le bucket émet un événement s3:ObjectCreated:* sans filtre de préfixe restrictif.
  3. Lambda invoquée : la fonction traite le fichier et écrit s3://mon-bucket/output/fichier_processed.csv.
  4. Nouvelle notification : l'écriture dans output/ déclenche à nouveau la même notification — la boucle commence.
  5. Explosion exponentielle : chaque invocation produit un nouvel objet, chaque objet produit une nouvelle invocation.

Le point critique : S3 ne sait pas que c'est votre Lambda qui a écrit l'objet. Pour S3, un PUT est un PUT, quelle qu'en soit l'origine.

Stopper la Boucle en Urgence — Boucle Infinie Lambda S3

Avant toute correction architecturale, coupez l'hémorragie. La priorité absolue est de stopper les invocations actives sans supprimer la fonction.

Étape 1 : Limiter la concurrence à zéro

Poser un reserved-concurrent-executions à 0 throttle immédiatement toutes les nouvelles invocations. Les événements en attente restent dans la file asynchrone d'AWS (pendant 6 heures maximum) — vous pourrez les traiter après correction, ou vider la file en supprimant temporairement le trigger.

aws lambda put-function-concurrency \
  --function-name ma-fonction-traitement \
  --reserved-concurrent-executions 0 \
  --region us-east-1

Vérifiez que le throttle est bien appliqué :

aws lambda get-function-concurrency \
  --function-name ma-fonction-traitement \
  --region us-east-1

Étape 2 : Supprimer le trigger S3 problématique

Identifier l'UUID de la notification S3 configurée sur la fonction — c'est ce qui alimente la boucle. Une fois la concurrence à zéro, les invocations sont throttlées mais les événements continuent d'être générés par S3 si des écritures se produisent encore. Supprimer le trigger coupe le flux à la source.

aws lambda list-event-source-mappings \
  --function-name ma-fonction-traitement \
  --region us-east-1

Les notifications S3 ne passent pas par list-event-source-mappings — elles sont configurées directement sur le bucket. Pour les lister :

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

Pour supprimer toutes les notifications (opération destructive — à utiliser uniquement en urgence) :

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

Étape 3 : Vérifier l'ampleur des dégâts

Combien d'invocations ont eu lieu ? Quel est l'état de la concurrence consommée ? Ces métriques sont dans CloudWatch sous le namespace AWS/Lambda.

aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=ma-fonction-traitement \
  --start-time 2024-01-15T10:00:00Z \
  --end-time 2024-01-15T11:00:00Z \
  --period 60 \
  --statistics Sum \
  --region us-east-1

Si vous voyez des milliers d'invocations par minute sur une courbe en escalier, c'est la signature caractéristique d'une boucle récursive — chaque vague est plus grande que la précédente jusqu'à saturation de la concurrence.

Diagnostiquer la Cause Racine

Une fois la boucle stoppée, il faut comprendre exactement pourquoi le filtre n'a pas fonctionné — ou pourquoi il n'existait pas.

Étape 4 : Inspecter la configuration de notification S3

La configuration de notification révèle si des filtres de préfixe ou de suffixe étaient en place. L'absence de filtre est la cause la plus fréquente.

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

Une configuration sans filtre ressemble à ceci :

🔽 Exemple de configuration sans filtre (dangereux)
{
  "LambdaFunctionConfigurations": [
    {
      "Id": "trigger-sans-filtre",
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ma-fonction-traitement",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {}
    }
  ]
}

Un filtre correctement configuré doit ressembler à ceci :

🔽 Exemple de configuration avec filtre de préfixe (correct)
{
  "LambdaFunctionConfigurations": [
    {
      "Id": "trigger-avec-filtre",
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ma-fonction-traitement",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [
            {
              "Name": "prefix",
              "Value": "input/"
            }
          ]
        }
      }
    }
  ]
}

Étape 5 : Identifier les objets déclencheurs dans CloudWatch Logs

Les logs Lambda contiennent le payload S3 complet pour chaque invocation. En inspectant la clé (key) de l'objet déclencheur, vous pouvez confirmer que la boucle provient bien d'écritures dans le préfixe de sortie.

aws logs filter-log-events \
  --log-group-name /aws/lambda/ma-fonction-traitement \
  --filter-pattern '"output/"' \
  --start-time 1705312800000 \
  --region us-east-1

Si les logs montrent des invocations déclenchées par des objets dans output/, la cause est confirmée : votre fonction écrit dans un préfixe qui est lui-même surveillé par la notification.

C'est comme poser un miroir en face d'un autre miroir : chaque reflet génère un nouveau reflet, à l'infini. La notification S3 ne distingue pas qui a écrit l'objet — elle réagit au fait qu'il existe.

Solutions Architecturales Permanentes

Il existe trois approches pour résoudre définitivement ce problème. Le choix dépend de vos contraintes opérationnelles.

graph LR subgraph SOL1["Solution 1 — Même bucket, préfixes séparés"] A1["input/fichier.csv"] -->|"Notification filtrée
préfixe: input/"| B1["Lambda"] B1 -->|"Écriture"| C1["output/fichier_processed.csv"] C1 -.->|"Pas de notification
préfixe non surveillé"| X1[" "] end subgraph SOL2["Solution 2 — Buckets séparés"] A2["bucket-source
fichier.csv"] -->|"Notification"| B2["Lambda"] B2 -->|"Écriture"| C2["bucket-destination
fichier_processed.csv"] C2 -.->|"Aucune notification
configurée"| X2[" "] end style SOL1 fill:#e8f5e9 style SOL2 fill:#e3f2fd

Solution 1 : Séparer les Préfixes Source et Destination (Recommandée)

C'est la solution la plus simple si vous devez rester sur le même bucket. Configurez la notification S3 pour ne surveiller que le préfixe input/, et écrivez systématiquement dans output/. Les deux préfixes ne se chevauchent jamais.

aws s3api put-bucket-notification-configuration \
  --bucket mon-bucket \
  --region us-east-1 \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "Id": "trigger-input-seulement",
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ma-fonction-traitement",
        "Events": ["s3:ObjectCreated:*"],
        "Filter": {
          "Key": {
            "FilterRules": [
              {
                "Name": "prefix",
                "Value": "input/"
              }
            ]
          }
        }
      }
    ]
  }'

Après application, restaurez la concurrence de la fonction :

aws lambda delete-function-concurrency \
  --function-name ma-fonction-traitement \
  --region us-east-1

Solution 2 : Buckets Séparés (La Plus Robuste)

Utiliser deux buckets distincts — un bucket source et un bucket destination — élimine complètement le risque de boucle, même en cas d'erreur de configuration future. La notification est configurée uniquement sur le bucket source, et la fonction écrit dans le bucket destination qui n'a aucune notification Lambda.

aws s3api put-bucket-notification-configuration \
  --bucket mon-bucket-source \
  --region us-east-1 \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "Id": "trigger-bucket-source",
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ma-fonction-traitement",
        "Events": ["s3:ObjectCreated:*"]
      }
    ]
  }'

La fonction Lambda doit écrire dans mon-bucket-destination, jamais dans mon-bucket-source.

Solution 3 : Détection de Boucle dans le Code Lambda

Cette approche est une défense en profondeur, pas un remplacement des solutions 1 ou 2. Elle consiste à inspecter la clé de l'objet déclencheur dans le payload S3 et à sortir immédiatement si l'objet provient du préfixe de sortie.

🔽 Exemple Python — Détection de boucle dans le handler Lambda
import json
import boto3

OUTPUT_PREFIX = 'output/'

def lambda_handler(event, context):
    for record in event.get('Records', []):
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        
        # Sortie immédiate si l'objet provient du préfixe de sortie
        if key.startswith(OUTPUT_PREFIX):
            print(f'Objet ignoré (préfixe de sortie détecté) : {key}')
            return {'statusCode': 200, 'body': 'Ignored - output prefix'}
        
        # Traitement normal
        process_file(bucket, key)
        
        # Écriture dans le préfixe de sortie
        output_key = OUTPUT_PREFIX + key.split('/')[-1].replace('.csv', '_processed.csv')
        write_output(bucket, output_key)
    
    return {'statusCode': 200, 'body': 'Processing complete'}


def process_file(bucket, key):
    # Logique de traitement
    pass


def write_output(bucket, key):
    s3 = boto3.client('s3')
    # Écriture du résultat
    pass

Cette garde-fou dans le code est utile, mais elle ne remplace pas un filtre S3 correct. Si quelqu'un modifie la notification S3 par erreur, le filtre dans le code reste actif.

Expérience de Production : Le Cas du Suffixe Trompeur

En production, j'ai vu une variante de ce problème qui a trompé toute l'équipe pendant deux heures. La notification S3 avait bien un filtre de préfixe sur input/. La boucle s'est quand même déclenchée.

Le symptôme : les logs montraient des invocations déclenchées par des objets dans input/ — exactement comme prévu. Mais le volume était anormal, environ 10x le nombre d'uploads réels.

Le diagnostic initial était faux : on cherchait une écriture dans output/ qui aurait déclenché la notification. Ce n'était pas ça.

La vraie cause : la fonction écrivait ses fichiers de sortie dans input/processed/fichier.csv — toujours sous le préfixe input/. Le filtre de préfixe correspondait, donc chaque fichier de sortie déclenchait une nouvelle invocation. La fonction détectait que le fichier était déjà traité (via un suffixe _processed) et sortait rapidement, mais l'invocation avait quand même lieu et générait un log.

La correction : déplacer les sorties vers output/ ou ajouter un filtre de suffixe négatif. En pratique, la solution la plus propre a été de passer à deux buckets distincts — le problème ne peut plus se reproduire par erreur de configuration.

Ce cas illustre un point non évident : un filtre de préfixe sur input/ ne protège pas si votre fonction écrit aussi sous input/. Le filtre doit être mutuellement exclusif avec le chemin d'écriture.

Mettre en Place des Garde-Fous Préventifs

Alarme CloudWatch sur les Invocations Lambda

Une alarme sur un pic soudain d'invocations peut vous alerter en quelques minutes avant que la boucle ne devienne catastrophique. Le seuil dépend de votre charge normale — définissez-le à 3-5x votre pic habituel.

aws cloudwatch put-metric-alarm \
  --alarm-name lambda-invocations-anormales \
  --alarm-description 'Détection de boucle récursive Lambda S3' \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=ma-fonction-traitement \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 2 \
  --threshold 1000 \
  --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:alertes-ops \
  --region us-east-1

Reserved Concurrency comme Disjoncteur

Même en production normale, définir un reserved-concurrent-executions raisonnable sur les fonctions déclenchées par S3 limite l'impact d'une boucle. Si la concurrence est épuisée, les nouvelles invocations sont throttlées plutôt que d'escalader indéfiniment.

aws lambda put-function-concurrency \
  --function-name ma-fonction-traitement \
  --reserved-concurrent-executions 50 \
  --region us-east-1

Attention : un reserved-concurrent-executions trop bas peut throttler des invocations légitimes en cas de pic normal. Calibrez selon votre charge maximale attendue.

IAM Policy Minimale pour la Fonction

Appliquer le principe du moindre privilège : la fonction ne doit avoir accès en lecture qu'au préfixe source, et en écriture qu'au préfixe destination. Cela ne prévient pas la boucle directement, mais limite les dégâts si elle se produit.

🔽 Politique IAM — Accès minimal source/destination
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LectureSourceUniquement",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::mon-bucket/input/*"
    },
    {
      "Sid": "EcritureDestinationUniquement",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::mon-bucket/output/*"
    }
  ]
}

Si la fonction tente d'écrire dans input/, l'appel PutObject échoue avec une erreur d'autorisation — ce qui est visible dans les logs et constitue un signal d'alarme supplémentaire.

Prochaines Étapes et Ressources — Boucle Infinie Lambda S3

La boucle récursive Lambda S3 est évitable avec une configuration rigoureuse dès le départ. Appliquez au minimum la Solution 1 (filtres de préfixe mutuellement exclusifs) et posez une alarme CloudWatch sur les invocations. Pour les pipelines critiques, la Solution 2 (buckets séparés) est la seule qui résiste aux erreurs de configuration futures.

Glossaire

TermeDéfinition
S3 Event NotificationMécanisme S3 qui émet un événement vers Lambda, SQS ou SNS lors d'opérations sur des objets (création, suppression, etc.).
Reserved ConcurrencyLimite de concurrence dédiée à une fonction Lambda spécifique, isolant son exécution du pool partagé du compte.
Préfixe S3Chemin partiel d'une clé d'objet S3 utilisé comme filtre dans les notifications (ex. : input/).
Throttling LambdaRejet d'une invocation Lambda lorsque la limite de concurrence est atteinte — l'événement est retourné à l'appelant ou mis en file d'attente selon le mode d'invocation.
Boucle récursiveSituation où la sortie d'une fonction constitue l'entrée qui la redéclenche, créant une chaîne d'invocations auto-alimentée.

Commentaires

Posts les plus consultés de ce blog

Structure de base d'une politique IAM : Effect, Action, Resource et Condition expliqués

LSI vs GSI dans DynamoDB : choisir le bon index secondaire

EBS vs EFS pour plusieurs instances EC2 : partager un dossier entre 5 serveurs