A la découverte d'Amazon DynamoDB Streams

Amazon DynamoDB

Qu'est-ce qu'Amazon DynamoDB Streams?

Annoncée durant l'été 2015, Amazon DynamoDB Streams est une mise à jour importante apportée au service de base de données NoSQL managée par Amazon.

DynamoDB Streams répond au besoin de certains utilisateurs de consigner les changements apportés à leurs tables DynamoDB (puts, updates, et deletes). Une fois la fonction Streams activée, la base gardera ces informations en mémoire durant 24 heures, ces dernières restant accessibles en quasi temps réel via le jeu d'API mis à disposition par Amazon. Dès lors, il est possible d'utiliser AWS Lambda suite à des événements remontés par Amazon DynamoDB Streams. Ces déclancheurs - ou triggers - vous permettront très simplement de traiter un certain nombre de tâches automatiquement : conditionner le scaling de la base, analyser les changements, demander la mise à jour de certaines données etc.


Introduction

Chez Osones, on est pragmatique: nous partons toujours d'un besoin. Aujourd'hui, on souhaite avoir une base de données d'utilisateurs comprenant nom, prénom et e-mail. Bien. Maintenant, on souhaite automatiser l'envoi d'un e-mail de bienvenue lors de l'insertion d'un nouvel utilisateur dans la base.

  • A l'attaque !


Fonctionnement

Pour arriver à ce but, nous allons utiliser les services Amazon suivants :

  • DynamoDB
  • DynamoDB Streams
  • IAM
  • Lambda
  • CloudWatch Logs
  • SES

DynamoDB sera utilisé pour stocker les données, DynamoDB Streams pour déclencher une fonction Lambda qui elle-même enverra un e-mail de bienvenue via SES.

Les autres services - IAM et CloudWatch Logs - sont là pour faire fonctionner le tout.

Amazon DynamoDB Lambda


Pas à pas

Création de la table DynamoDB

On commence donc par créer une table que l'on appelle osones :

aws dynamodb create-table \
        --table-name osones \
        --attribute-definitions AttributeName=email,AttributeType=S \
        --key-schema AttributeName=email,KeyType=HASH \
        --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
        --stream-specification StreamEnabled=True,StreamViewType=NEW_IMAGE

Commande qui nous renvoie ceci :

{
    "TableDescription": {
        "TableArn": "arn:aws:dynamodb:eu-west-1:xxxxxxxxxxxx:table/osones", 
        "AttributeDefinitions": [
            {
                "AttributeName": "email", 
                "AttributeType": "S"
            }
        ], 
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0, 
            "WriteCapacityUnits": 1, 
            "ReadCapacityUnits": 1
        }, 
        "TableSizeBytes": 0, 
        "TableName": "osones", 
        "TableStatus": "CREATING", 
        "StreamSpecification": {
            "StreamViewType": "NEW_IMAGE", 
            "StreamEnabled": true
        }, 
        "LatestStreamLabel": "2016-02-26T13:23:10.193", 
        "KeySchema": [
            {
                "KeyType": "HASH", 
                "AttributeName": "email"
            }
        ], 
        "ItemCount": 0, 
        "CreationDateTime": 1456492990.19, 
        "LatestStreamArn": "arn:aws:dynamodb:eu-west-1:xxxxxxxxxxxx:table/osones/stream/2016-02-26T13:23:10.193"
    }
}

Notez bien la ligne suivante, on en aura besoin plus tard :

"LatestStreamArn": "arn:aws:dynamodb:eu-west-1:xxxxxxxxxxxx:table/osones/stream/2016-02-26T13:23:10.193"

Voilà, la table est créée.


Role IAM Lambda

Pour que DynamoDB Streams puisse parler à Lambda, et que notre fonction Lambda puisse elle-même parler à SES, il faut donner les bons droits à notre fonction.

On commence par créer un rôle IAM :

cat > iam-role.json << EOF
{
   "Version": "2012-10-17",
   "Statement": [
   {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {"Service":"lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
   }]
}
EOF

Puis :

aws iam create-role \
   --role-name lambda-dynamodb \
   --assume-role-policy-document file://iam-role.json

Une fois que le rôle est créé, on peut écrire la policy :

cat > iam-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "lambda:InvokeFunction"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetRecords",
        "dynamodb:GetShardIterator",
        "dynamodb:DescribeStream",
        "dynamodb:ListStreams",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "ses:*"
      ],
      "Resource": "*"
    }
  ]
}
EOF

Puis :

aws iam put-role-policy \
   --role-name lambda-dynamodb \
   --policy-name lambda-dynamodb \
   --policy-document file://iam-policy.json


Fonction Lambda de test

Nous sommes prêts, on peut maintenant écrire une petite fonction Lambda de test, pour voir si tout fonctionne.

cat > envoi_email.js << EOF
console.log('=== Début ===');

exports.handler = function(event, context) {
    event.Records.forEach(function(record) {
        console.log(record.eventID);
        console.log(record.eventName);
        console.log('DynamoDB Record: %j', record.dynamodb);
    });
    context.succeed("== Fin ==");
};
EOF

Une fois que cette fonction est écrite, on la zip :

zip envoi_email.zip envoi_email.js

Puis on peut envoyer cette fonction dans Lambda :

aws lambda create-function \
        --function-name envoi_email \
        --runtime nodejs \
        --role arn:aws:iam::xxxxxxxxxxxx:role/lambda-dynamodb \
        --handler envoi_email.handler \
        --zip-file fileb://envoi_email.zip

Commande qui nous renvoie ceci :

{
    "FunctionName": "envoi_email", 
    "CodeSize": 357, 
    "MemorySize": 128, 
    "FunctionArn": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:envoi_email", 
    "Handler": "envoi_email.handler", 
    "Role": "arn:aws:iam::xxxxxxxxxxxx:role/lambda-dynamodb", 
    "Timeout": 3, 
    "LastModified": "2016-02-26T13:55:00.389+0000", 
    "Runtime": "nodejs", 
    "Description": ""
}

Cette fonction permet juste de d'afficher dans CloudWatch Logs les paramètres.

Il faut maintenant faire en sorte que cette fonction soit déclenchée lorsqu'un nouvel enregistrement est inséré dans notre table DynamoDB.

aws lambda create-event-source-mapping \
        --event-source-arn arn:aws:dynamodb:eu-west-1:xxxxxxxxxxxx:table/osones/stream/2016-02-26T13:23:10.193 \
        --function-name envoi_email \
        --enabled \
        --starting-position LATEST

Ce qui renvoie :

{
    "UUID": "2a3b31a5-ed00-48e5-a5c2-5fe052639d87", 
    "StateTransitionReason": "User action", 
    "LastModified": 1456495304.215, 
    "BatchSize": 100, 
    "EventSourceArn": "arn:aws:dynamodb:eu-west-1:xxxxxxxxxxxx:table/osones/stream/2016-02-26T13:23:10.193", 
    "FunctionArn": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:envoi_email", 
    "State": "Creating", 
    "LastProcessingResult": "No records processed"
}

Voilà, nous sommes fins prêt, nous allons pouvoir ajouter un enregistrement dans notre base


Enregistrement d'un item

On commence donc par créer un petit fichier JSON :

cat > item.json << EOF
{
        "email":  {"S": "alexis.gunst@osones.com"},
        "nom":    {"S": "GÜNST HORN"},
        "prenom": {"S": "Alexis"}
}
EOF

Puis on injecte ça dans DynamoDB :

aws dynamodb put-item \
        --table-name osones \
        --item file://item.json \
        --return-consumed-capacity TOTAL

Normalement, cette action a déclenché la fonction Lambda. Allons vérifier.


Lecture des logs

Lambda écrit dans CloudWatch Logs. Allons donc voir ce qu'il en est.

On commence par lister les streams disponibles :

aws logs describe-log-streams \
   --log-group-name /aws/lambda/envoi_email \
   --query 'logStreams[*].logStreamName' \
   --output table
----------------------------------------------------------
|                   DescribeLogStreams                   |
+--------------------------------------------------------+
|  2016/02/26/[$LATEST]1b3343a141eb437a9f642444b6b6e360  |
+--------------------------------------------------------+

Il suffit donc de lire ce stream :

aws logs get-log-events \
   --log-group-name /aws/lambda/envoi_email \
   --log-stream-name '2016/02/26/[$LATEST]1b3343a141eb437a9f642444b6b6e360'\
   --query 'events[*].message'

Ce qui renvoie ceci :

[
    "START RequestId: e00bc024-a1a4-47f3-9921-409f0dc2f086 Version: $LATEST\n", 
    "2016-02-26T14:44:47.826Z\te00bc024-a1a4-47f3-9921-409f0dc2f086\t=== Début ===\n", 
    "2016-02-26T14:44:47.827Z\te00bc024-a1a4-47f3-9921-409f0dc2f086\ta701e6bd4ec41f8454f6d6e5f309836a\n", 
    "2016-02-26T14:44:47.827Z\te00bc024-a1a4-47f3-9921-409f0dc2f086\tINSERT\n", 
    "2016-02-26T14:44:47.827Z\te00bc024-a1a4-47f3-9921-409f0dc2f086\tDynamoDB Record: {\"Keys\":{\"email\":{\"S\":\"alexis.gunst@osones.com\"}},\"NewImage\":{\"nom\":{\"S\":\"GÜNST HORN\"},\"prenom\":{\"S\":\"Alexis\"},\"email\":{\"S\":\"alexis.gunst@osones.com\"}},\"SequenceNumber\":\"1300000000003218571175\",\"SizeBytes\":82,\"StreamViewType\":\"NEW_IMAGE\"}\n", 
    "END RequestId: e00bc024-a1a4-47f3-9921-409f0dc2f086\n", 
    "REPORT RequestId: e00bc024-a1a4-47f3-9921-409f0dc2f086\tDuration: 0.58 ms\tBilled Duration: 100 ms \tMemory Size: 128 MB\tMax Memory Used: 27 MB\t\n"
]

Victoire ! On voit qu'on a bien reçu l'événement d'insertion. Et si on met un peu en forme les données reçues, on obtient ceci :

{
    "Keys": {
        "email": {
            "S": "alexis.gunst@osones.com"
        }
    },
    "NewImage": {
        "nom": {
            "S": "GÜNST HORN"
        },
        "prenom": {
            "S": "Alexis"
        },
        "email": {
            "S": "alexis.gunst@osones.com"
        }
    },
    "SequenceNumber": "1300000000003218571175",
    "SizeBytes": 82,
    "StreamViewType": "NEW_IMAGE"
}

On a donc tout ce qu'il faut pour envoyer un e-mail.


Envoi d'un e-mail

Maintenant qu'on a réussi à lier DynamoDB à Lambda via DynamoDB Streams, il ne reste plus qu'à modifier un peu notre fonction Lambda et faire en sorte qu'elle puisse envoyer un émail via SES.

Nouvelle fonction lambda :

cat > envoi_email.js << EOF
var AWS = require('aws-sdk') ;

exports.handler = function(event, context) {

        console.log(event);
        event.Records.forEach(function(record) {
                if (record.eventName == 'INSERT') {

                        var ses = new AWS.SES();

                        var email  = record.dynamodb.Keys.email.S ;
                        var prenom = record.dynamodb.NewImage.prenom.S ;
                        var nom    = record.dynamodb.NewImage.nom.S ;

                        var params = {
                                Destination: { ToAddresses: [ email ] },

                                Message: {
                                        Body: {
                                                Text: {
                                                        Data: 'Bienvenue '+ prenom + ' ' + nom + ' !\n\nMerci de valider votre e-mail: https://s3-eu-west-1.amazonaws.com/agh-osones/valider.html?email='+email,
                                                        Charset: 'UTF-8'
                                                }
                                        },
                                        Subject: {
                                                Data: 'Bienvenue '+ prenom + ' ' + nom + ' !',
                                                Charset: 'UTF-8'
                                        }
                                },
                                Source: 'alexis.gunst@osones.com',
                        };

                        console.log(params);

                        ses.sendEmail (params, function(err, data) {
                                if (err) context.fail (err) ;
                                else     context.succeed (data) ;
                        });
                }
        });
};      
EOF

On zipe :

zip envoi_email.js envoi_email.zip

Et on envoie :

Mise à jour de la fonction
aws lambda update-function-code \
        --function-name envoi_email \
        --zip-file fileb://envoi_email.zip


Test final

On peut recommencer notre test (ajout d'un nouvel utilisateur)... Et oh ! Miracle ! Le mail est bien parti !

Amazon DynamoDB envoi de mail


Alexis GÜNST HORN

C'est à vous de jouer !

Questions, remarques, suggestions... Contactez-nous directement sur Twitter sur @osones !

Pour discuter avec nous de vos projets, nous restons disponibles directement via contact@osones.com ou via le chat !

- Encore un peu de temps ? Parcourez nos dossiers :

A la découverte d'AWS Lambda

AWS Lambda


A la découverte d'Amazon DynamoDB Streams

Amazon DynamoDB


Container as a Service avec Amazon EC2 Container Service (ECS)

Amazon ECS


On a testé Amazon Elastic File System (EFS)

Amazon EFS


Rejoignez VOTRE groupe LinkedIn dès maintenant : Utilisateurs Francophones d'Amazon Web Services (AWS).

AWS user group FR

La discussion continue !

Nous attendons vos questions, remarques & mots doux sur notre Twitter :