Dans cet article, je vous propose de partir à la découverte des lambda AWS et de voir un cas d’usage qui m’a été bien
pratique.
Comme très souvent dans mes posts, je pars d’un problème pour en trouver une solution.
Problème à résoudre
Charger des images dans mon bucket S3 sans se préoccuper de leur taille afin d’éviter de les redimensionner manuellement à chaque chargement
J’ai été confronté à ce problème avec la partie blog de Clever Tech Ware 😉
Solution
Plusieurs solutions existent dans l’écosystème AWS S3 :
- utiliser le service SNS/SQS pour déclencher un programme qui redimensionne l’image à chaque chargement,
- utiliser le service Lambda pour exécuter une fonction à chaque chargement d’image dans le bucket S3,
- utiliser le service Lambda@Edge pour exécuter une fonction à chaque chargement d’image dans le bucket S3,
Pour mon besoin, j’ai choisi la solution consistant à redimensionner l’image à chaque chargement dans le bucket S3 en utilisant le service Lambda.
Pour des questions de coûts, la 1ère solution ne m’allait pas car il faut payer pour le service SNS/SQS et pour le service EC2 qui exécute le programme de redimensionnement. Pour mon cas d’usage, je préfère ne pas stocker l’image dans son dimensionnement original et j’ai donc écarté la 3e solution également.
La solution retenue est donc la suivante :
A chaque chargement d’une image dans le bucket S3 dit “source”, une fonction Lambda est exécutée pour redimensionner l’image et la stocker dans un autre bucket S3 dit “destination”.
Prérequis
- Un compte AWS
- 2 buckets S3
- une CLI AWS configurée
Réalisation de la solution
Nous suivrons scripuleusement le didacticiel officiel d’AWS : Utilisation d’un déclencheur Amazon S3 pour créer des images.
Variable d’environnement pour les tests
On va démarrer par créer des variables d’environnement qui nous permettront de variabiliser les principaux paramètres de notre solution.
On définira les variables d’environnement suivantes :
SOURCE_BUCKET
: le nom du bucket S3 source,DESTINATION_BUCKET
: le nom du bucket S3 destination,AWS_REGION
: la région AWS,AWS_ACCOUNT_ID
: l’identifiant de votre compte AWS.
export SOURCE_BUCKET=<bucket-name-source>
export DESTINATION_BUCKET=${SOURCE_BUCKET}-thumbnail
export AWS_REGION=eu-west-3
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
Création d’un bucket s3
On commence par créer les 2 buckets S3 source et destination :
aws s3 mb s3://${SOURCE_BUCKET}
aws s3 mb s3://${DESTINATION_BUCKET}
Charger une image de test dans votre compartiment source
On récupère une image que l’on va charger dans notre bucket source :
## On récupère une image de test
curl -L -o photo.jpeg https://unsplash.com/photos/zZSH36b7VIY/download?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjk0Mjg1MjgxfA&force=true
aws s3 cp photo.jpeg s3://${SOURCE_BUCKET}/photo.jpeg
Note: On peut aussi utiliser l’api s3api
aws s3api put-object --bucket ${SOURCE_BUCKET} --key photo.jpeg --body ./photo.jpeg
Créer les permissions nécessaires à lambda
La lambda aura besoin de réaliser les opérations suivantes sur AWS :
- créer un log dans CloudWatch et y écrire des logs,
- lire les objets du bucket source,
- écrire les objets dans le bucket destination.
cat <<EOF > lambda-function-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:PutLogEvents",
"logs:CreateLogGroup",
"logs:CreateLogStream"
],
"Resource": "arn:aws:logs:${AWS_REGION}:${AWS_ACCOUNT_ID}:*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::${SOURCE_BUCKET}/*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::${DESTINATION_BUCKET}/*"
}
]
}
EOF
aws iam create-policy --policy-name LambdaS3Policy --policy-document file://lambda-function-policy.json
Créer le rôle d’exécution de la lambda et affecter les permissions
On va créer désormais le rôle IAM que nous pourrons associer à notre lambda. Cela lui permettra d’obtenir les permissions nécessaires pour accéder aux ressources AWS au travers de l’application de ce rôle à la lambda.
cat <<EOF > lambda-role-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
aws iam create-role --role-name LambdaS3Role --assume-role-policy-document file://lambda-role-trust-policy.json
aws iam attach-role-policy --role-name LambdaS3Role --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/LambdaS3Policy
Désormais, nous pouvons créer notre lambda.
Créer le package de déploiement de la lambda
Pour notre lambda, nous choisirons le langage python, plutôt bien adapté pour le traitement d’image.
Nous allons nous créer un environnement virtuel python pour installer les dépendances de notre lambda au sein de notre projet lambda-package.
mkdir lambda-package
cd lambda-package
python3 -m venv venv
source venv/bin/activate
Créer le fichier lambda_function.py
avec le contenu suivant:
import boto3
import os
import sys
import uuid
from urllib.parse import unquote_plus
from PIL import Image
import PIL.Image
s3_client = boto3.client('s3')
def resize_image(image_path, resized_path):
with Image.open(image_path) as image:
image.thumbnail(tuple(x / 2 for x in image.size))
image.save(resized_path)
def lambda_handler(event, context):
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = unquote_plus(record['s3']['object']['key'])
tmpkey = key.replace('/', '')
download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
upload_path = '/tmp/resized-{}'.format(tmpkey)
s3_client.download_file(bucket, key, download_path)
resize_image(download_path, upload_path)
s3_client.upload_file(upload_path, '{}-thumbnail'.format(bucket), 'resized-{}'.format(key))
On va maintenant installer les dépendances de notre lambda:
pip install \
--platform manylinux2014_x86_64 \
--target=package \
--implementation cp \
--python-version 3.9 \
--only-binary=:all: --upgrade \
pillow boto3
On peut maintenant sortir du venv
:
deactivate
Enfin, on crée le package de déploiement de notre lambda :
cd package
zip -r ../lambda-package.zip .
cd ..
zip -g lambda-package.zip lambda_function.py
Note : quand votre lambda a besoin de dépendance, il est nécessaire de créer un package de déploiement contenant les dépendances et le code de la lambda via une archive zip.
Créer la lambda
Pour créer la lambda, on va utiliser la commande aws lambda create-function
:
aws lambda create-function \
--function-name CreateThumbnail \
--zip-file fileb://lambda-package.zip \
--handler lambda_function.lambda_handler \
--runtime python3.9 \
--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/LambdaS3Role \
--region ${AWS_REGION}
Configurer S3 pour invoquer la fonction
On configure d’abord notre bucket S3 pour qu’il puisse invoquer la fonction lambda:
aws lambda add-permission \
--function-name CreateThumbnail \
--principal s3.amazonaws.com \
--statement-id s3invoke \
--action "lambda:InvokeFunction" \
--source-arn arn:aws:s3:::${SOURCE_BUCKET} \
--source-account ${AWS_ACCOUNT_ID}
Ensuite, on paramètre notre bucket source pour invoquer la fonction lambda à chaque chargement d’image :
cat <<EOF > s3-notification-config.json
{
"LambdaFunctionConfigurations": [
{
"Id": "CreateThumbnailEventConfiguration",
"LambdaFunctionArn": "arn:aws:lambda:${AWS_REGION}:${AWS_ACCOUNT_ID}:function:CreateThumbnail",
"Events": [ "s3:ObjectCreated:Put" ]
}
]
}
EOF
aws s3api put-bucket-notification-configuration \
--bucket ${SOURCE_BUCKET} \
--notification-configuration file://s3-notification-config.json
Test de la fonction à l’aide d’un évènement de test
Afin de tester notre lambda, on peut utiliser un évènement de test :
cat <<EOF > s3-dummy-event.json
{
"Records":[
{
"eventVersion":"2.0",
"eventSource":"aws:s3",
"awsRegion":"${AWS_REGION}",
"eventTime":"1970-01-01T00:00:00.000Z",
"eventName":"ObjectCreated:Put",
"userIdentity":{
"principalId":"AIDAJDPLRKLG7UEXAMPLE"
},
"requestParameters":{
"sourceIPAddress":"127.0.0.1"
},
"responseElements":{
"x-amz-request-id":"C3D13FE58DE4C810",
"x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
},
"s3":{
"s3SchemaVersion":"1.0",
"configurationId":"testConfigRule",
"bucket":{
"name":"${SOURCE_BUCKET}",
"ownerIdentity":{
"principalId":"A3NL1KOZZKExample"
},
"arn":"arn:aws:s3:::${SOURCE_BUCKET}"
},
"object":{
"key":"photo.jpeg",
"size":1024,
"eTag":"d41d8cd98f00b204e9800998ecf8427e",
"versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko"
}
}
}
]
}
EOF
aws lambda invoke --function-name CreateThumbnail \
--invocation-type RequestResponse --cli-binary-format raw-in-base64-out \
--payload file://s3-dummy-event.json outputfile.txt
Vérification du résultat
On va vérifier dans le bucket destination que l’image est redimensionnée :
aws s3 ls s3://${DESTINATION_BUCKET}
Ou avec l’api s3api
aws s3api list-objects-v2 --bucket ${DESTINATION_BUCKET}
Usage de la lambda
Si tout se passe bien, alors désormais, vous n’aurez plus qu’à charger vos images dans le bucket source et elles seront redimensionnées automatiquement dans le bucket destination.
Nettoyage
Pour nettoyer votre environnement, vous pouvez supprimer les ressources créées :
aws s3 rm s3://${SOURCE_BUCKET}/photo.jpeg
aws s3 rm s3://${DESTINATION_BUCKET}/resized-photo.jpeg
aws s3 rb s3://${SOURCE_BUCKET}
aws s3 rb s3://${DESTINATION_BUCKET}
aws iam detach-role-policy --role-name LambdaS3Role --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/LambdaS3Policy
aws iam delete-policy --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/LambdaS3Policy
aws iam delete-role --role-name LambdaS3Role
aws lambda delete-function --function-name CreateThumbnail
Pour aller plus loin
Découvrir le framework Serverless pour déployer des fonctions lambdas AWS.
Resources utiles
- Didacticiel : Utilisation d’un déclencheur Amazon S3 pour créer des images miniatures
- Didacticiel : utilisation d’un déclencheur Amazon S3 pour invoquer une fonction Lambda
- Déploiement des fonctions Lambda comme des archives de fichiers .zip
- Deploying an AWS Lambda function with the serverless framework and Tekton
- Serverless Framework
- AWS Lambda function with Serverless Framework
- How to handle AWS Lambda timeout issues