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!
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.
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...)
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'appelertoto-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
A la découverte d'Amazon DynamoDB Streams
Container as a Service avec Amazon EC2 Container Service (ECS)
On a testé Amazon Elastic File System (EFS)
Rejoignez VOTRE groupe LinkedIn dès maintenant : Utilisateurs Francophones d'Amazon Web Services (AWS).
Découvrez les derniers articles d'alter way
- : Prowler : L'outil de sécurité multi-cloud indispensable pour renforcer votre infrastructure
- : Kubernetes : plateforme "star" de l'IT et levier d'innovation des entreprises
- AI_dev2024
- : DirectPV : Avoir du stockage bloc distribué facilement dans kubernetes
- : Simple comme GitOps : kluctl
- Conférence Wax 2024 @thecamp