Le Blog Amazon Web Services

Nouveauté pour AWS Lambda – Prise en charge des images de conteneur

Avec AWS Lambda, vous chargez votre code et l’exécutez sans que vous ayez à mettre en service ou à gérer des serveurs. De nombreux clients apprécient ce mode de fonctionnement, mais si vous avez investi dans la conteneurisation lors de vos cycles de développement, il n’était pas facile d’utiliser la même approche pour créer des applications à l’aide de Lambda.

Pour vous aider, vous pouvez désormais packager et déployer des fonctions Lambda sous forme d’images de conteneur d’une taille maximale de 10 Go. Vous pouvez aussi facilement construire et déployer des applications plus importantes qui reposent sur des dépendances de taille plus important, comme le Machine Learning ou les traitements intensifs de données. Tout comme les fonctions packagées sous forme d’archives ZIP, les fonctions déployées sous forme d’images de conteneur bénéficient de la même simplicité opérationnelle, de la mise à l’échelle automatique, de la haute disponibilité et des intégrations natives avec de nombreux services AWS.

Nous fournissons des images de base pour tous les moteurs d’exécution Lambda pris en charge (Python, Node.js, Java, .NET, Go, Ruby) afin que vous puissiez facilement ajouter votre code et vos dépendances. Nous proposons également des images de base pour les moteurs d’exécution personnalisés basés sur Amazon Linux que vous pouvez étendre pour inclure votre propre moteur d’exécution implémentant l’API Lambda Runtime.

Vous pouvez déployer vos propres images de base sur Lambda, par exemple des images basées sur les distributions Linux Alpine ou Debian. Pour fonctionner avec Lambda, ces images doivent implémenter l’API Lambda Runtime. Pour faciliter la construction de vos propres images de base, nous publions des clients d’interface d’exécution Lambda qui implémentent l’API Lambda Runtime pour tous les environnements d’exécution pris en charge. Ces implémentations sont disponibles via les gestionnaires de paquets natifs, afin que vous puissiez facilement les intégrer dans vos images, et sont partagées avec la communauté sous une licence Open Source.

Nous lançons également en Open Source un émulateur d’interface d’exécution Lambda (Lambda Runtime Interface Emulator) qui vous permet d’effectuer des tests en local de l’image du conteneur et de vérifier qu’elle fonctionnera lorsqu’elle sera déployée vers Lambda. Le Lambda Runtime Interface Emulator est inclus dans toutes les images de base fournies par AWS et peut également être utilisé avec toute autre image.

Vos images de conteneur peuvent également utiliser l’API Lambda Extensions pour intégrer des outils de surveillance, de sécurité et autres à l’environnement d’exécution Lambda.

Pour déployer une image de conteneur, vous en sélectionnez une dans un référentiel Amazon Elastic Container Registry (ECR). Voyons comment cela fonctionne en pratique à l’aide de quelques exemples, en utilisant d’abord une image fournie par AWS pour Node.js, puis en créant une image personnalisée pour Python.

Utilisation de l’image de base fournie par AWS pour Node.js
Voici le code (app.js) d’une simple fonction Lambda Node.js générant un fichier PDF à l’aide du module PDFKit. À chaque invocation, elle crée un nouveau mail contenant des données aléatoires générées par le module faker.js. La sortie de la fonction utilise la syntaxe d’Amazon API Gateway pour renvoyer le fichier PDF.

const PDFDocument = require('pdfkit');
const faker = require('faker');
const getStream = require('get-stream');

exports.lambdaHandler = async (event) => {

    const doc = new PDFDocument();

    const randomName = faker.name.findName();

    doc.text(randomName, { align: 'right' });
    doc.text(faker.address.streetAddress(), { align: 'right' });
    doc.text(faker.address.secondaryAddress(), { align: 'right' });
    doc.text(faker.address.zipCode() + ' ' + faker.address.city(), { align: 'right' });
    doc.moveDown();
    doc.text('Dear ' + randomName + ',');
    doc.moveDown();
    for(let i = 0; i < 3; i++) {
        doc.text(faker.lorem.paragraph());
        doc.moveDown();
    }
    doc.text(faker.name.findName(), { align: 'right' });
    doc.end();

    pdfBuffer = await getStream.buffer(doc);
    pdfBase64 = pdfBuffer.toString('base64');

    const response = {
        statusCode: 200,
        headers: {
            'Content-Length': Buffer.byteLength(pdfBase64),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=test.pdf'
        },
        isBase64Encoded: true,
        body: pdfBase64
    };
    return response;
};

Nous utilisons npm pour initialiser le paquet et ajouter les trois dépendances dont nous avons besoin dans le fichier package.json. De cette façon, nous créons également le fichier package-lock.json. Nous l’ajoutons à l’image du conteneur pour avoir un résultat plus reproductible.

$ npm init
$ npm install pdfkit
$ npm install faker
$ npm install get-stream

Maintenant, nous créons un fichier Dockerfile pour créer l’image du conteneur pour notre fonction Lambda, en partant de l’image de base fournie par AWS pour le moteur d’exécution nodejs12.x. Toutes les images de base fournies par AWS sont disponibles sur Docker Hub et Amazon ECR Public. Dans ce cas, nous utilisons l’image de base hébergée dans Docker Hub :

FROM amazon/aws-lambda-nodejs:12
COPY app.js package*.json ./
RUN npm install
CMD [ "app.lambdaHandler" ]

Pour utiliser l’image dans Amazon ECR Public, nous pouvons remplacer la première ligne par :

FROM public.ecr.aws/lambda/nodejs:12

Le fichier Dockerfile ajoute le code source (app.js) et les fichiers décrivant le paquet et les dépendances (package.json et package-lock.json) à l’image de base. Ensuite, nous lançons npm pour installer les dépendances. Nous avons défini le CMD sur le gestionnaire de fonction, mais cela pourrait également être surchargé lors de la configuration de la fonction Lambda.

Nous utilisons la CLI de Docker pour construire localement l’image du conteneur random-letter :

$ docker build -t random-letter .

Pour vérifier si cela fonctionne, nous démarrons l’image du conteneur localement en utilisant l’émulateur d’interface d’exécution Lambda :

$ docker run -p 9000:8080 random-letter:latest

Maintenant, nous testons une invocation de fonction avec cURL. Ici, nous passons une payload JSON vide.

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

S’il y a des erreurs, nous pouvons les corriger localement. Si cela fonctionne, nous passons à l’étape suivante.

Pour charger l’image du conteneur, nous créons un nouveau référentiel Amazon ECR dans notre compte et nous balisons l’image locale pour la pousser vers ECR. Pour nous aider à identifier les vulnérabilités logicielles dans nos images de conteneur, nous activons l’analyse d’image ECR.

$ aws ecr create-repository --repository-name random-letter --image-scanning-configuration scanOnPush=true
$ docker tag random-letter:latest 123412341234.dkr.ecr.eu-west-3.amazonaws.com/random-letter:latest
$ aws ecr get-login-password | docker login --username AWS --password-stdin 123412341234.dkr.ecr.eu-west-3.amazonaws.com
$ docker push 123412341234.dkr.ecr.eu-west-3.amazonaws.com/random-letter:latest

Ici, nous utilisons la console AWS pour terminer la création de la fonction. Vous pouvez également utiliser l’AWS Serverless Application Model (SAM), qui a été mis à jour pour ajouter la prise en charge des images de conteneurs.

Dans la console Lambda, nous cliquons sur Créer une fonction. nous sélectionnons Image de conteneur, donnons un nom à la fonction, puis nous cliquons sur Parcourir les images pour rechercher la bonne image dans mes référentiels ECR.

Screenshot of the console.

Après avoir sélectionné le référentiel, nous utilisons la dernière image (latest, qui est pratique pour nos tests, mais qui n’est pas forcément recommandé en production) que nous avons chargée. Lorsque nous sélectionnons l’image, la Lambda la traduit en empreinte numérique (digest) de l’image sous-jacente (à droite de la balise dans l’image ci-dessous). Vous pouvez voir l’empreinte numérique de vos images localement avec la commande docker images --digests. De cette façon, la fonction utilise la même image même si la dernière balise est passée à une plus récente, et vous êtes protégés contre les déploiements involontaires. Vous pouvez mettre à jour l’image à utiliser dans le code de la fonction. La mise à jour de la configuration de la fonction n’a aucun impact sur l’image utilisée, même si la balise a été réaffectée à une autre image entre-temps.

En option, nous pouvons remplacer certaines des valeurs de l’image du conteneur. Nous ne le faisons pas maintenant, mais de cette façon, nous pouvons créer des images qui peuvent être utilisées pour différentes fonctions, par exemple en modifiant le gestionnaire de fonction dans la valeur CMD.

Nous laissons toutes les autres options à leur valeur par défaut et nous sélectionnons Créer une fonction.

Lors de la création ou de la mise à jour du code d’une fonction, la plateforme Lambda optimise les nouvelles images de conteneurs et leurs mises à jour pour les préparer à recevoir des invocations. Cette optimisation prend quelques secondes voire minutes, en fonction de la taille de l’image. Après cela, la fonction est prête à être invoquée. Nous testons la fonction dans la console depuis l’onglet Tester puis en cliquant sur Tester.

Cela fonctionne ! Maintenant, ajoutons l’API Gateway comme déclencheur. Nous sélectionnons Ajouter un déclencheur et ajoutons l’API Gateway en utilisant une API HTTP. Pour simplifier, nous laissons l’API sans authentification pour nos tests (en production, il est recommandé d’avoir une stratégie d’authentification pour maîtriser les accès).

Maintenant, nous cliquons plusieurs fois sur le point de terminaison (endpoint) de l’API et nous téléchargeons quelques mails au hasard.

Cela fonctionne comme prévu ! Voici quelques-uns des fichiers PDF qui sont générés avec des données aléatoires provenant du module faker.js.

Construction d’une image personnalisée pour Python

Il est parfois nécessaire d’utiliser vos images de conteneur personnalisées, par exemple pour respecter les directives de votre entreprise ou pour utiliser une version du moteur d’exécution que nous ne prenons pas en charge.

Dans ce cas, nous souhaitons construire une image pour utiliser Python 3.9. Le code (app.py) de notre fonction est très simple, nous voulons juste dire bonjour avec la version de Python qui est utilisée.

import sys
def handler(event, context):
    return 'Hello from AWS Lambda using Python' + sys.version + '!'

Comme nous l’avons déjà mentionné, nous partageons avec vous des implémentations Open Source des clients de l’interface d’exécution Lambda (qui mettent en œuvre la Runtime API) pour tous les environnements d’exécution pris en charge. Dans ce cas, nous commençons par une image Python basée sur Alpine Linux. Ensuite, nous ajoutons le client Lambda Runtime Interface Client for Python à l’image. Voici le fichier Dockerfile :

# Définir les arguments globaux
ARG FUNCTION_DIR="/home/app/"
ARG RUNTIME_VERSION="3.9"
ARG DISTRO_VERSION="3.12"

# Etage 1 - Packager l'image de base et le moteur d'exécution
# Prendre une copie récente de l'image et installer GCC
FROM python:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} AS python-alpine

# Installer GCC (Alpine utilise musl mais nous compilons et lions les dépendances avec GCC)
RUN apk add --no-cache \
    libstdc++
    
# Etape 2 - construire la fonction et les dépendances
FROM python-alpine AS build-image
# Installer les dépendances de aws-lambda-cpp
RUN apk add --no-cache \
    build-base \
    libtool \
    autoconf \
    automake \
    libexecinfo-dev \make \
    cmake \
    libcurl
    
# Inclure les arguments globaux dans cette étape de construction
ARG FUNCTION_DIR
ARG RUNTIME_VERSION

# Créer un répertoire pour la fonction
RUN mkdir -p ${FUNCTION_DIR}

# Copier le gestionnaire de la fonction
COPY app/* ${FUNCTION_DIR}

# Optionnel – Installer les dépendances de la fonction
# RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}
# Installer Lambda Runtime Interface Client for Python
RUN python${RUNTIME_VERSION} -m pip install awslambdaric --target ${FUNCTION_DIR}

# Etape 3 - Image finale du moteur d'exécution
# Obtenir la dernière version de l'image Python
FROM python-alpine

# Inclure les arguments globaux dans cette étape de la construction
ARG FUNCTION_DIR
# Définir le répertoire de travail au répertoire racine de la fonction
WORKDIR ${FUNCTION_DIR}

# Copier dans les dépendances construites
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}

# (Facultatif) Ajouter le Lambda Runtime Interface Emulator et utiliser un script dans l'ENTRYPOINT pour simplifier les exécutions locales
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
COPY entry.sh /
RUN chmod 755 /usr/bin/aws-lambda-rie /entry.sh
ENTRYPOINT [ "/entry.sh" ]
CMD [ "app.handler" ]

Cette fois, le fichier Dockerfile est plus détaillé et construit l’image finale en trois étapes, conformément aux meilleures pratiques de Docker en matière de construction en plusieurs étapes. Vous pouvez utiliser cette approche en trois étapes pour construire vos propres images personnalisées :

  • L’étape 1 consiste à construire l’image de base avec le moteur d’exécution, Python 3.9 dans ce cas, et GCC que nous utilisons pour compiler et lier les dépendances à l’étape 2,
  • L’étape 2 consiste à installer le client d’interface d’exécution Lambda (Lambda Runtime Interface Client) et à construire la fonction et les dépendances,
  • L’étape 3 consiste à créer l’image finale en ajoutant le résultat de l’étape 2 à l’image de base construite à l’étape 1. Ici,  nous ajoutons aussi le Lambda Runtime Interface Emulator, mais c’est optionnel (voir ci-dessous).

Nous créons le script entry.sh ci-dessous pour l’utiliser comme ENTRYPOINT. Il exécute le client d’interface d’exécution Lambda pour Python (Lambda Runtime Interface Client for Python). Si l’exécution est locale, le client d’interface d’exécution est enveloppé par le Lambda Runtime Interface Emulator.

#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
    exec /usr/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $1
else
    exec /usr/local/bin/python -m awslambdaric $1
fi

Maintenant, nous pouvons utiliser le Lambda Runtime Interface Emulator pour vérifier localement si la fonction et l’image du conteneur fonctionnent correctement :

$ docker run -p 9000:8080 lambda/python:3.9-alpine3.12

Sans inclure l’émulateur d’interface d’exécution Lambda dans l’image du conteneur

Il est facultatif d’ajouter l’émulateur d’interface d’exécution Lambda à une image de conteneur personnalisée. Si nous ne l’incluons pas, nous pouvons tester localement en installant le Lambda Runtime Interface Emulator sur notre machine locale en suivant ces étapes :

  • Dans l’étape 3 du fichier Dockerfile, nous supprimons les commandes copiant le Lambda Runtime Interface Emulator (aws-lambda-rie) et le script entry.sh. Nous n’avons pas besoin du script entry.sh dans ce cas.
  • Nous utilisons cet ENTRYPOINT pour démarrer par défaut le client Lambda Runtime Interface :

ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]

  • Nous exécutons ces commandes pour installer le Lambda Runtime Interface Emulator sur notre machine locale, par exemple sous ~/.aws-lambda-rie :
mkdir -p ~/.aws-lambda-rie
curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
chmod +x ~/.aws-lambda-rie/aws-lambda-rie

Lorsque le Lambda Runtime Interface Emulator est installé sur notre machine locale, nous pouvons le monter lors du démarrage du conteneur. La commande pour démarrer le conteneur localement est maintenant (en supposant que le Lambda Runtime Interface Emulator se trouve dans ~/.aws-lambda-rie) :

docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \
       --entrypoint /aws-lambda/aws-lambda-rie lambda/python:3.9-alpine3.12
       /entry.sh app.handler

Tester l’image personnalisée pour Python

Lorsque le conteneur est exécuté localement, nous pouvons tester l’invocation d’une fonction avec cURL :

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

Le résultat est conforme à nos attentes !

"Hello from AWS Lambda using Python3.9.0 (default, Oct 22 2020, 05:03:39) \n[GCC 9.3.0]!"

Nous chargeons l’image sur ECR et crée la fonction comme précédemment. Voici notre test dans la console :

Notre image de conteneur personnalisée basée sur Alpine fait tourner Python 3.9 sur Lambda !

Disponible dès maintenant

Vous pouvez utiliser des images de conteneurs pour déployer vos fonctions Lambda dès aujourd’hui.

La prise en charge des images de conteneurs est proposée en plus des archives ZIP et nous continuerons à prendre en charge le format de package ZIP.

Il n’y a pas de frais supplémentaires pour utiliser cette fonctionnalité. Vous payez pour le référentiel ECR et le coût Lambda habituel.

Vous pouvez utiliser la prise en charge des images de conteneur dans AWS Lambda avec la console, l’interface de ligne de commande (CLI) d’AWS, les SDK d’AWS, le Serverless Application Model (SAM) d’AWS, le Cloud Development Kit (CDK) d’AWS, les boîtes à outils AWS pour Visual Studio, VS Code et JetBrains, et les solutions des partenaires AWS, notamment Aqua Security, Datadog, Epsagon, HashiCorp Terraform, Honeycomb, Lumigo, Pulumi, Stackery, Sumo Logic et Thundra.

Article original contribué par Danilo Poccia, Chief Evangelist (EMEA) et adapté en français par Charles Rapp, Architecte de Solutions dans l’équipe AWS France.