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 :

Image de test

## 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