A la découverte d'AWS Lambda

Amazon Lambda

Qu'est-ce qu' AWS Lambda ?

AWS Lambda est un des nouveaux services AWS annoncés durant le RE:invent 2014, la keynote annuelle d'AWS. Encore en version beta, AWS Lambda est un service de calcul qui exécute votre code en réponse à des événements que vous determinez, soit en provenance d'un service Amazon - Lambda supporte aujoud'hui Amazon S3, Amazon DynamoDB, Amazon Kinesis, Amazon SNS, et Amazon Cognito - soit en provenance d'un événement extérieur invoqués via le SDK AWS et le SDK AWS Mobile.

Que ce soit pour créer des miniatures de vos images, valider à la volée des adresses dans vos applications, ou générer des achats in-app dans vos jeux pour mobile, vous n'aurez plus à vous préoccuper de l'infrastructure : en bon service managé, les ressources nécessaires au traitement de vos événements sont automatiquement mises à disposition par le Cloud Amazon. Vous n'avez plus qu'à nourrir la bête de votre code ! AWS Lambda supporte pour le moment Java et Node.JS, mais d'autres langages devraient arriver prochainement.

Enfin, si le service est proposé à un prix compétitif, il reste compliqué d'en évaluer le montant. Il vous faudra en effet croiser le nombre de requêtes mensuelles (1 million de requêtes gratuites par mois, puis 0,20 USD par million de requêtes supplémentaire) et la durée d'exécution de ces requêtes. Et comme c'était trop simple, cette durée est elle même conditionnée par la quantité de mémoire utilisée. On parlera alors de Go-seconde, chacune de ces Go-secondes vallant 0,00001667 USD.

Une fois la phobie administrative passée, le service AWS Lambda facilite réellement la vie, et risque de changer beaucoup de choses dans le monde du Cloud. Voyons pourquoi.


Objectif : redimensionner des images

Comme d'habitude, j'aime travailler à partir d'exemples concrets. Ici, notre but sera le suivant : faire en sorte que lorsqu'une image est poussée dans un bucket S3, une vignette soit créée et palcée dans un second bucket.

Pour arriver à ce résultat, plusieurs voies s'offrent à nous.

Avant

Avant, pour traiter ce genre de problèmes, nous avions le choix :

Méthode lourde

Sur une instance dédiée, un cron se déclenche toutes les minutes, va lister le contenu du bucket S3 si une nouvelle image est présente, la télécharge, fait le traitement de redimensionnement, puis la pousse dans le second bucket.

Méthode Best practice

La méthode propre est typiquement basée sur Amazon SQS. Lors de l'upload de l'image, un message est envoyé dans une file SQS. Sur une instance dédiée, on lit en permanence cette file. Dès qu'un nouveau message arrive, alors on lance le traitement de redimensionnement et d'upload.

C'est mieux, plus propre, plus scalable. Bref, plus cloud. Et ça fonctionne.

Maintenant

... suspense... Maintenant, on utilise... Lambda ! Bravo!

AWS Lambda

Concrètement, Lambda s'appelle Lambda à cause des fonctions lambda. Ici une fonctione lambda est une fonction javascript qui va être exécutée en réponse à un évenement. Dans notre exemple, l'événement "Une nouvelle image est arrivée dans le bucket S3".

Vous remarquerez que je ne parle plus d'instance dédiée... Et oui ! C'est là que se trouve la révolution Lambda. Il n'est plus nécessaire d'avoir une instance EC2 dédiée au traitement. AWS s'en charge pour nous. Notre fonction lambda est exécutée dans le cloud. Si la fonction ne prend que 300ms à s'éxecuter, on ne payera que 300ms.


Étape 1 - Hello World

Il faut bien passer par là. Nous allons faire un simple Hello World avec Lambda. Pour commencer, cette fonction sera déclenchée à la main, par nous, via la ligne de commande.

Création de la fonction

On commence par écrire une fonction en Javascript très compliquée qui affichera Bonjour Prénom Nom !

exports.handler = function (event, context)
{
   var nom    = event.nom ;
   var prenom = event.prenom ;

   console.succeed ("Bonjour "+ prenom + " " + nom +" !") ;
}

Rien de bien compliqué. La fonction exécutée en réponse à un évenement se trouve dans handler. La fonction context.succeed quant à elle affiche d'une part un message dans CloudWatch Logs et indique d'autre part à Lambda que tout s'est bien déroulé.

Autorisations IAM

Comme notre fonction va écrire dans CloudWatch logs, il faut qu'elle ait les bons droits. Nous allons donc créer un role d'exécution Lambda possédant ces droits.

On commence donc par créer le rôle IAM (dans un fichier temporaire) :

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

Puis on créée pour de vrai notre rôle :

aws iam create-role \
   --role-name lambda-exec \
   --assume-role-policy-document file:///tmp/iam

Notez bien ici l'ARN du rôle que l'on vient de créer. On en aura besoin par la suite. Dans notre cas, on obtient :

arn:aws:iam::397960517128:role/lambda-exec

Maintenant que le rôle existe, on va pouvoir lui attribuer les bons droits, à savoir le droit en ecriture dans CloudWatch Logs :

cat > /tmp/iam << EOF
{
   "Version": "2012-10-17",
   "Statement": [
   {
      "Action": [
         "logs:*"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:logs:*:*:*"
   }]
}
EOF

Puis :

aws iam put-role-policy \
   --role-name lambda-exec \
   --policy-name acces-a-cloudwatch \
   --policy-document file:///tmp/iam

Envoi de la fonction Lambda

Il faut maintenant uploader notre fonction Lambda. Mais avant ça, il faut packager notre code dans un fichier ZIP. Ceci permet en fait d'envoyer des applications Node.js complètes, avec toutes les librairies nécessaires. Nous c'est simple, nous n'avons qu'un fichier.

On commence donc par créer un fichier zip :

zip hello.zip hello.js

Puis on l'envoie par pigeon voyageur (cf. RFC 2549) à Lambda :

aws lambda create-function \
   --function-name hello \
   --runtime nodejs \
   --role arn:aws:iam::397960517128:role/lambda-exec \
   --handler hello.handler \
   --zip-file fileb://hello.zip

Et voilà, notre fontion lambda est envoyée. Il ne reste plus qu'a l'utiliser.

AWS Lambda

Appel de la fonction lambda

Nous allons appeler la fonction Lambda via la ligne de commande. On ommence par écrire un fichier avec nos paramètres (mon prénom et mon nom) au format JSON.

cat > /tmp/input << EOF
{
   "prenom": "Alexis",
   "nom": "GÜNST HORN"
}
EOF

Puis, on appelle notre fonction lambda, avec les paramètres définis plus haut.

aws lambda invoke-async \
   --function-name hello \
   --invoke-args /tmp/input

Comme son nom l'indique, invoke-async fait un appel asynchrone à notre fonctione lambda. On n'a donc ici comme retour, si tout s'est bien passé, qu'un code :

{
   "Status": 202
}

Pour lire la sortie de notre fonction, il va falloir aller fouiller dans CloudWatch logs. On commence par récuperer le nom du stream

aws logs describe-log-streams \
   --log-group-name /aws/lambda/hello \
   --query 'logStreams[*].logStreamName' \
   --output text

Puis on va lire le stream

aws logs get-log-events \
   --log-group-name /aws/lambda/hello \
   --log-stream-name 170ab6bfaa344bb5984cff1330bcccd8 \
   --query 'events[*].message'

Et on voit bien ceci :

START RequestId: af3(...)b1b
2015-03-02T10:05:42.469Z  af36(...)b1b  Message: "Bonjour Alexis GÜNST HORN !
END RequestId: af36(...)b1b
REPORT RequestId: af36(...)b1b  Duration: 7.60 ms  Billed Duration: 100 ms  Memory Size: 128 MB  Max Memory Used: 9 MB

Et voilà, on a bien notre message ! La fonction a bien été appelée, et on obtient en prime quelques informations :

  • la fonction a mis 7,60 ms a s'éxecuter (mais AWS nous facture 100ms)
  • la fonction a eu besoin de 9 Mo de RAM pour fonctionner


Étape 2 - redimensionnement d'images

Le but du jeu ici est donc de créer des vignettes automatiquement, sitôt qu'une image arrive dans un bucket S3.

Pour ce faire, nous allons utiliser des tableaux croisés dynamiques dans Excel, avec des macros VisualBasic dans Access. .. Mais bien sûr... Non ! Nous allons utiliser Lambda ! (petite forme ce matin le rédacteur...)

C'est cela oui...

Pas à pas

Environnement de travail

Comme nous l'avons vu dans la première partie, les fonctions Lambda sont des fonctions JavaScript exécutées par Node.js. (scoop : c'est tout nouveau tout chaud, une version Java de Lambda est désormais disponible...).

Afin de développer notre fonction lambda, il nous faut donc Node.js.

Sous Ubuntu :

sudo apt-add-repository ppa:chris-lea/node.js
sudo apt-get install nodejs npm

Plaçons nous maintenant là où nous souhaitons écrire notre fonction, et installons maintenant le module NPM gm. GM comme Graphics Magick. Ce module permet de manipuler Graphics Magick, qui lui-même est un fork de Image Magick.

mkdir ~/lambda
cd ~/lambda
npm install gm
vim miniature.js

Fonction Lambda

Nous allons donc maintenant écrire notre fonction Lambda :

// Chargement des modules Node.js
var AWS = require('aws-sdk') ;
var gm  = require('gm').subClass({ imageMagick: true }) ;
var s3  = new AWS.S3() ;


// Fonction Lambda
exports.handler = function(event, context)
{

   //--[ Variables globales ]-----------------------------------------

   // Bucket S3 source
   var srcBucket = event.Records[0].s3.bucket.name ;

   // Objet source (sans les espaces)
   var srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")) ;


   // On vérifie qu'on a bien affaire a une image jpg ou png
   var typeMatch = srcKey.match(/\.([^.]*)$/) ;

   if (!typeMatch)
      context.fail ("Impossible de déterminer l'extension du fichier !") ;

   var imageType = typeMatch[1] ;

   if (imageType != "jpg" && imageType != "png")
      context.fail ("Le fichier n'est ni un JPG ni un PNG !") ;



   //--[ Fonctions S3 ]-----------------------------------------------
   function get_object_from_s3(cb)
   {
    s3.getObject ({ Bucket: srcBucket, Key: srcKey }, cb) ;
   }

   function put_object_s3(params, cb)
   {
    s3.putObject(params, function(err, data) {
        if (err) context.fail ('Erreur dans put_object_s3') ;
        cb() ;
    }) ;
   }


   //--[ Fonctions ImageMagick ]--------------------------------------
   function transform(data, cb)
   {

      gm (data.Body)
         .resize (400)
         .toBuffer(imageType, function(err, buffer) {
        if (err) context.fail('Erreur dans transform_m') ;

        // Upload dans S3
        var params = {  Bucket: srcBucket+'-mini',
                Key: srcKey,
                Body: buffer } ;

        put_object_s3 (params, cb) ;
    }) ;
   }


   function done(err)
   {
    console.log ('********** FIN DE LA FONCTION ************') ;
    context.succeed () ;
   }


   function run()
   {
    console.log ('******** ENTREE DANS LA FONCTION *********') ;

    get_object_from_s3(function (err, data) {
        if (err) context.fail('Impossible de joindre S3 !');
        transform(data, done);
    });
   }

   run () ;
};

Remarques importantes :

  • Le bucket source doit exister
  • Le bucket de destination doit exister. Si le source s'appelle toto, celui de destination doit s'appeler toto-mini.
  • Si un expert Node.js passe dans le coin, qu'il reste assis et pardonne ma façon quelque peu cavalière d'écrire : je ne suis pas développeur Javascript...

Permissions IAM

On l'a vu, notre code va d'une part récupérer une image depuis un bucket, et uploader dans un autre. Il faut donc donner les droits a notre rôle Lambda sur les buckets agh-osones et agh-osones-mini

cat > /tmp/iam << EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::agh-osones/*",
                "arn:aws:s3:::agh-osones-mini/*"
            ]
        }
    ]
}
EOF

puis

aws iam put-role-policy \
   --role-name lambda-exec \
   --policy-name acces-a-s3 \
   --policy-document file:///tmp/iam

Envoi du code dans Lambda

On considère que le rôle IAM a bien été créé (cf. première partie). On peut donc envoyer notre fonction lambda.

Mais avant, il faut créer notre package sous forme de zip :

zip -r miniature.zip ./*

Puis on l'envoie dans Lambda :

aws lambda create-function \
   --function-name miniature \
   --runtime nodejs \
   --role arn:aws:iam::397960517128:role/lambda-exec \
   --handler miniature.handler \
   --zip-file fileb://miniature.zip

Configuration de S3 pour interagir avec Lambda

C'est bien joli tout cela, nous avons une fonction Lambda prête à être déclenchée dès qu'un objet arrive dans un bucket S3. Certes... mais comment demander à S3 de déclencher cet évenement ?

On commence par autoriser le bucket S3 a déclencher la fonction Lambda

aws lambda add-permission \
   --function-name miniature \
   --statement-id $(uuidgen) \
   --action "lambda:InvokeFunction" \
   --principal s3.amazonaws.com  \
   --source-arn arn:aws:s3:::agh-osones \
   --source-account 397960517128

Puis, on configure les notifications au niveau du bucket. Pour ce faire, on commence par écrire un fichier temporaire de configuration :

cat > /tmp/notif << EOF
{
   "CloudFunctionConfiguration":
   {
      "InvocationRole": "",
      "CloudFunction": "arn:aws:lambda:eu-west-1:397960517128:function:miniature",
      "Id": "toto",
      "Event": "s3:ObjectCreated:Put"
   }
}
EOF
aws s3api put-bucket-notification \
   --bucket agh-osones \
   --notification-configuration file://tmp/notif

Test grandeur nature

Maintenant, si vous envoyez des photos dans le premier bucket, alors, quelques secondes après, vous aurez la version miniature dans le second bucket.

Preuve :

aws s3 ls s3://agh-osones/
(vide)

aws s3 ls s3://agh-osones-mini/
(vide)

On envoie maintenant une photo dans le bucket agh-osones :

aws s3 cp /tmp/ma-super-photo.jpg s3://agh-osones/
echo "je joue un peu la montre" && sleep 2
aws s3 ls s3://agh-osones-mini/
TODO

Magique non ?

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 :