Dans mon précédent article, j’ai évoqué le cas où un fichier déposé sur Amazon S3 devait déclencher une notification SNS à notre application. Dans cet article, on va rentrer dans le vif du sujet en détaillant, étape par étape, comment faire pour arriver à obtenir ce comportement.

Comprendre le contexte

“Amazon S3”, “SNS”, c’est quoi ?

  • Amazon S3 ou “Amazon Simple Storage Service”, c’est, comme son nom l’indique, le système de stockage d’Amazon. Il est simple dans le sens où il se comporte comme un gros dossier sur votre ordinateur : on peut ranger les fichiers dans des sous-dossiers et chaque fichier se trouve dans un chemin.

  • SNS, ça veut dire “Simple Notification System”. Dans l’univers AWS (le service de cloud computing d’Amazon) c’est le protocole le plus simple pour faire communiquer deux machines. AWS c’est vaste, il y a plein de services et bien souvent, ces services communiquent entre eux avec SNS.

Comprendre SNS avec un cas concret : la transformation des PDF en TXT

Chez Deepki, on doit gérer des millions de factures. Toutes ces factures, on les stocke sur S3. La grande majorité de ces factures sont au format PDF mais il est intéressant pour nous de les manipuler au format TXT. Pour obtenir deux versions d’une même facture, l’une en PDF et l’autre en TXT, à chaque fois qu’on dépose une facture PDF dans son dossier sur S3, une copie au format TXT va apparaître dans le dossier des TXT comme par magie.

De la magie, vous dites ? Que nenni ! Ce qu’il s’est passé, c’est que le dépôt de la facture a déclenché l’envoi d’un message SNS. Ce message a été communiqué par SNS à un autre service d’AWS : une lambda qui va, elle, exécuter un bout de code qui va transformer le PDF en TXT.

Le parcours de la facture

L’avantage principal de cette méthode, c’est sa simplicité. De notre côté, la seule chose qu’on a eu à faire, c’est :

  • configurer S3 pour qu’il déclenche cet appel SNS,
  • écrire le code de la lambda qui réalise la transformation PDF->TXT.

Une fois que ça, c’est fait, chaque facture sera transformée automatiquement. L’autre avantage, c’est qu’on adopte un fonctionnement événementiel : lorsque la facture arrive sur S3, elle est automatiquement transformée en TXT, il n’y a pas besoin d’attendre le passage d’un script ou je ne sais quoi.

Ça a l’air très pratique tout ça ! Mais Amazon gère déjà tout, non ?

Dans beaucoup de cas, c’est vrai : AWS est très bien fait et on trouve souvent son bonheur dans l’ensemble des services qu’ils proposent. C’était le cas de l’exemple précédent où il n’était pas nécessaire d’abonner un service extérieur.

Mais il arrive que l’on veuille aller plus loin et dans ces cas-là, AWS a souvent tout prévu aussi. En ce qui concerne SNS, les services d’Amazon dialoguent très bien entre eux sans que l’on ait à mettre son nez dedans. Mais si je veux que ce soit mon application (et non un service AWS) qui soit notifiée quand un fichier est déposé sur S3, alors mon application doit pouvoir recevoir des appels SNS.

Et pour cela, il n’y a pas le choix : il va falloir développer un module qui sait recevoir des notifications SNS.

Le tutoriel

Allons-y, implémentons le protocole SNS ! Où est-ce qu’on commence ?

Tout d’abord, on va supposer que l’on a une application web qui tourne avec une API tout ce qu’il y a de plus banal. Ce qu’il va falloir faire, c’est exposer un nouvelle route HTTP dédiée à la réception de notifications SNS.

Mais avant même de recevoir notre premier appel SNS, il faut tout d’abord prouver à Amazon que notre application a bien implémenté le protocole SNS et est capable de recevoir des notifications. Si c’est le cas, on peut alors l’abonner à un topic SNS. Une fois abonnée, elle recevra donc les notifications SNS que l’on aura configuré sur AWS et on pourra alors exploiter ces notifications dans notre application.

En résumé :

I. On ajoute à notre application le code qui lui permettra de recevoir des messages SNS.
II. On fait en sorte qu’un nouveau fichier sur S3 envoie un message SNS à notre application.
III. On traite le fichier qui vient d’arriver dans notre application.

I. Implémentation du protocole SNS

Avant de parler de topic, il faut implémenter le protocole sur un endpoint que l’on va exposer.

Pour vérifier que notre endpoint marche correctement, AWS envoie une notification d’abonnement à notre route et notre route doit être capable de lui répondre correctement. (Et de la même manière, même s’il n’est pas nécessaire de pouvoir se désabonner, on va aussi implémenter la réponse à un désabonnement.)

AWS a implémenté le protocole SNS dans leur SDK pour Java. Manque de bol, chez Deepki, notre application est une application Flask donc en Python. Et malheureusement, la validation de message SNS n’est pas disponible dans le SDK pour Python.

Mais heureusement, la communauté Python est toujours là pour nous soutenir dans ce genre de désagrément. En effet, il existe une librairie qui fait exactement ce dont nous avons besoin : SNS Message Validator On installe cette librairie à son projet avec :

pip install git+https://github.com/wlwg/sns-message-validator.git

Comme ils l’expliquent dans leur README, cette librairie offre un validateur de message SNS ainsi qu’un exemple d’utilisation avec Flask, ce qui est donc très facile à ajouter à notre application. Puisque le message de la réponse n’est pas utile, j’ai adapté ce qu’il y avait dans flask_example.py dans une fonction sns_status_code qui prend en argument la méthode qui doit exploiter le fichier qui a été ajouté et qui retourne le statut qu’il faut répondre :

def sns_status_code(message_handler):
    logger = logging.getLogger('my_app')
    sns_message_validator = SNSMessageValidator()

    # First, validate the message type from header without having to parse the request body.
    message_type = request.headers.get('x-amz-sns-message-type')
    try:
        sns_message_validator.validate_message_type(message_type)
        message = json.loads(request.data)
        sns_message_validator.validate_message(message=message)
    except Exception as ex:
        logger.error(ex)
        return 400

    # Then, we say what the app should respond to AWS when it wants to subscribe/unsubscribe.
    subscription_url_keys = {
        SNSMessageType.SubscriptionConfirmation.value: 'SubscribeURL',
        SNSMessageType.UnsubscribeConfirmation.value: 'UnsubscribeURL'
    }
    if message_type in subscription_url_keys:
        url_key = subscription_url_keys[message_type]
        resp = requests.get(message.get(url_key))
        if 200 > resp.status_code >= 300:
            logger.error(resp)
            return 500
        return 200

    # Finally, we do what we want to do with the message
    message_handler(message.get('Message'))
    return 200

Je me sers donc de cette fonction dans le endpoint que je définis comme suit :

@api_blueprint.route('/sns', methods=['POST'])
def sns():
    status = sns_status_code(handle_sns_message))
    return Response(" ", status=status)  # Le message renvoyé n'a pas d'importance

Et pour l’instant, ma fonction handle_sns_message va juste afficher le message avec lequel elle est appelée :

def handle_sns_message(message):
    logger.info(f'SNS notification received : {message}')

Et avec tout ça, notre application tourne en local et devrait être capable de s’abonner à un topic SNS. On la met de côté pour l’instant, on y reviendra à l’étape III.

II. Notifier notre application quand un fichier est déposé sur s3

Cette étape peut soit être faite directement sur la console AWS, soit avec terraform.

Méthode A : sur la console AWS

1 - S’abonner à un topic SNS

  • Tout d’abord, se connecter à la console AWS.
  • Une fois arrivé sur notre compte, on est accueilli par la liste des services d’AWS. Dans cette liste, il faut trouver “Simple Notification Service” dans la catégorie “Application Integration”.

  • Cliquer sur Topics dans la marge à gauche.

  • Là, on arrive sur une page avec la liste de nos topics. Pour créer notre topic, on clique sur le bouton “Create topic” en haut à droite.

  • Lors de la création du topic :
    • Nommer le nouveau topic
    • Actuellement, il y a un bug chez AWS qui fait que l’access policy qui est donnée par défaut n’est pas correcte. Pour la changer, il faut :
      • Ouvrir le volet Access policy,
      • Sélectionner Advanced,
      • Changer la clef de la ligne 25 de AWS:SourceOwner à AWS:SourceAccount (la valeur, elle, reste la même).
    • Cliquer sur le bouton “Create topic” en bas de page.

  • Ça y est, le topic “tutoriel-sns” est créé !
La page de notre nouveau topic

2 - Tester la souscription en local avec ngrok

Il est courant de tester d’abord son code en local avant de le mettre en production. Sauf que vous l’aurez compris, ici, c’est un peu compliqué : AWS va essayer d’appeler notre route qui, pour l’instant, n’existe qu’en local.

Alors afin de tester proprement en local, je vous recommande ngrok. Ngrok est un service gratuit qui permet d’exposer son serveur local sur une URL publique. Leur service est d’excellente facture et l’installation est d’une simplicité déconcertante, il suffit de suivre leurs indications qui sont extrêmement claires.

  • Une fois l’installation faite, on lance ngrok avec la commande suivante :
./ngrok http 5002

(Pensez bien à remplacer le numéro de port par celui de votre application.)

  • Normalement, à ce moment-là, votre terminal doit afficher quelque chose comme ça :

On repère notamment l’url exposée par ngrok (ici http://e2846c1f.ngrok.io) et on la note.

  • Revenons maintenant à la page de notre topic et cliquons sur “Create subscription” :

  • On sélectionne le protocole “HTTP” dans la liste et on renseigne comme endpoint l’url de ngrok suivie de notre route. Puis, on valide en cliquant sur “Create subscription”

  • On arrive sur la page de notre souscription. On peut notamment voir qu’AWS est en attente de notre confirmation.

  • Jetons un coup d’oeil au terminal. On voit que l’on a bien reçu un POST :

Ce qui veut dire qu’AWS a envoyé une notification SNS à ngrok qui l’a passée à notre application qui a pu répondre correctement au message de souscription.

  • Si l’on recharge la page de notre souscription, on peut voir que la souscription est bel et bien confirmée.

L’objectif est réussi. On va maintenant pouvoir envoyer nos premières notifications SNS.

NB: Une fois déployé en prod, il faudra à nouveau créer une souscription en répétant ces étapes. En particulier, on pourra en profiter pour utiliser le protocole HTTPS.

3 - Envoyer des notifications SNS quand on ajoute un fichier sur S3

  • Dans la liste des services AWS, aller sur S3 puis cliquer sur le nom du bucket qui nous intéresse

  • Aller dans “Properties”

  • Ouvrir le panneau “Events”

  • Cliquer sur “Add notification”

  • Ici il faut :
    • nommer l’event,
    • sélectionner “All object create events”,
    • dans “Send to”, entrer “SNS Topic”,
    • dans “SNS”, entrer le nom de notre topic,
    • et enfin sauvegarder.

Et ça y est, désormais, tout objet déposé dans notre bucket notifiera notre application.

Pour tester, on peut déposer un fichier dans le bucket et on peut alors constater qu’un POST a bien été transmis par ngrok et que notre application a bien affiché le log de réception.

Dernière ligne : le POST passé par ngrok
Le log de l'application : le message est bien reçu

Méthode B : avec terraform

Si on choisit d’utiliser terraform, voici un fichier de configuration qui reproduit le même comportement pour un bucket donné :

# La configuration de notre bucket s3
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-bucket"
  acl    = "private"
}

# Le topic SNS qui n'autorise que ce qui vient de notre bucket
resource "aws_sns_topic" "tutoriel_sns" {
  name = "tutoriel-sns"

  policy = <<POLICY
{
    "Version":"2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {"AWS":"*"},
        "Action": "SNS:Publish",
        "Resource": "arn:aws:sns:*:*:tutoriel-sns",
        "Condition": {
            "ArnLike":{"aws:SourceArn":"${aws_s3_bucket.my_bucket.arn}"}
        }
    }]
}
POLICY

}

# On souscrit notre application à notre topic SNS
resource "aws_sns_topic_subscription" "tutoriel_sns_subscription" {
  topic_arn = aws_sns_topic.tutoriel_sns.arn
  protocol  = "https"
  endpoint  = "https://my.app.url/sns"  # À remplacer
  endpoint_auto_confirms = true
}

# La notification est déclenchée quand un objet est créé
resource "aws_s3_bucket_notification" "publish_sns_to_app" {
  bucket = aws_s3_bucket.my_bucket.id

  topic {
    topic_arn     = aws_sns_topic.tutoriel_sns.arn
    events        = ["s3:ObjectCreated:*"]
  }
}

III. Exploiter les fichiers qui ont été déposés

Maintenant que l’on reçoit nos premières notifications SNS, il va donc falloir les exploiter. Pour cela, il suffit donc d’étoffer la fonction handle_sns_message que l’on a mis de côté jusqu’à présent.

Généralement, on va organiser notre bucket d’une certaine manière et on voudra effectuer des traitements différents en fonction du sous-dossier d’où provient le fichier qui vient d’être déposé.

Voilà donc un exemple de traitement :

BUCKET = 'your.bucket.name'

def handle_sns_message(message):
    s3_path = extract_key_from_message(message)
    if s3_path is None:
        return
    if s3_path.startswith('directory_1/sub_directory_A/'):
        treat_files_1_A(s3_path)
    elif s3_path.startswith('directory_1/sub_directory_B/'):
        treat_files_1_B(s3_path)
    elif s3_path.startswith('directory_2/'):
        file = get_sns_file(s3_path)
        treat_files_2(file)
    else:
        logger.warning('%s: cannot handle this kind of s3_path yet', s3_path)


def extract_key_from_message(message):
    try:
        return json.loads(message)['Records'][0]['s3']['object']['key']
    except Exception:
        logger.exception('Can\'t find the key in sns message : %r', message)
        return None

def get_sns_file(s3_path):
    return boto3.resource('s3').Bucket(BUCKET).Object(s3_path).get()['Body']

Il ne reste donc plus qu’à écrire treat_files_1_A, treat_files_1_B et treat_files_2 pour y décrire le traitement que l’on veut faire pour chaque sous-dossier. Pour l’exemple, on a fait le choix arbitraire de ne passer le chemin de s3 uniquement pour le traitement des fichiers du dossier directory_1 alors que l’on passe directement le fichier à treat_files_2. Cela va évidemment dépendre de ce que l’on voudra en faire.

La seule limite est votre imagination.

Conclusion

Et voilà ! Nos fichiers se font traiter automatiquement par notre application quand ils sont déposés sur S3.

Cependant, lors de ce tutoriel, nous n’avons qu’effleuré ce qu’AWS a à offrir. En effet, SNS réserve bien d’autres surprises ! Tout au long du tutoriel, nous sommes passés devant de nombreuses options sur la console AWS dont nous n’avons pas parlé. En vrac : on peut déclencher des notifications SNS pour d’autres types d’évènements, dire quoi faire si la notification n’est pas reçue, être plus spécifique en ce qui concerne l’access policy, etc.

N’hésitez pas à y jeter un oeil ! Ce n’est qu’en explorant par vous-mêmes que vous progresserez !