Lambda est un outil AWS très puissant. L'exécution de scripts en mode serverless permet de réduire drastiquement les coûts et la complexité d'avoir à gérer une infrastructure scalable, néanmoins, tester directement ses fonctions sur Lambda peut parfois être frustrant, car nécessitant des allers retours entre le poste de développement et l'environnement AWS.

Il existe des fonctionnalités de tests intégrées au toolkit AWS pour les éditeurs les plus populaires (pour Microsoft Visual Studio Code / PyCharm, par exemple), néanmoins, cela restreint les éditeurs possibles et créé une adhérence que l'on ne souhaite pas particulièrement.

Aujourd'hui, je vous propose de voir comment tester vos fonctions Lambda simplement avec Docker.

Pour les besoins de ce billet, je ferais mes explications en me basant sur un environnement d'exécution Python3, qui correspond à un langage que je maîtrise. Pour faire les mêmes actions dans le langage que vous souhaitez, je vous invite à vous reporter à la documentation de l'image docker.

Lambci/lambda : une image pour les tester tous!

Lambci/Lambda est une image disponible sur dockerhub permettant de simuler un environnement d'exécution Lambda.

Cette image va nous permettre d'émuler une fonction lambda localement, pour effectuer des tests.

Il est aussi possible de simuler un serveur web pour recréer une lambda derrière un ALB ou une API Gateway.

Préparation de mon environnement de travail

Cette étape est purement optionnelle, elle consiste à créer simplement une arborescence de travail me permettant ensuite de créer mon package lambda en .zip.

Créer un répertoire pour mon test

Bonne pratique de développement python en règle générale, je vais créer un répertoire dans lequel je vais déposer mon script, mes éventuels layers et mon fichier requirement.txt, contenant les librairies tierces dont je vais avoir besoin pour mon test. J'ai donc créé l'arborescence suivante :

.
├── lambda
│   ├── main.py
│   └── requirements.txt
└── layers

L'arborescence /lambda me servira pour créer mon script lambda et indiquer ses requirements, le /layers me permettra de créer mes layers, que j'aborderais dans un second temps.

Créer une première lambda, avec une librairie externe

Je vais créer une simple fonction lambda qui va utiliser la librairie python requests pour faire un appel http sur mon site. Cette librairie n'est pas installée de base sur lambda, je devrais donc l'installer moi-même.

#!/usr/bin/env python3
import requests

def lambda_handler(event, context):
    r = requests.get('https://tferdinand.net')
    print('Return code : {}'.format(r.status_code))
    if r.status_code == 200:
        return 0
    else:
        return 1

Le code est très simpliste, mais ce n'est pas le sujet de ce billet.

Exécuter mon code python dans un contexte lambda

Maintenant que ma fonction est prête (et quelle fonction!), il est temps de la tester.

Dans un premier temps, je vais renseigner mon fichier lambda/requirements.txt avec la seule ligne "requests", ce module n'étant pas présent de base dans lambda.

Créer une nouvelle image avec mes librairies et mon code

Pour ce faire, je vais créer un nouveau fichier "Dockerfile" à la racine de mon répertoire de développement.

.
├── Dockerfile
├── lambda
│   ├── main.py
│   └── requirements.txt
└── layers

Dans ce Dockerfile, je vais :

FROM lambci/lambda:python3.8
COPY ./lambda/ .
USER root
RUN pip install -r requirements.txt

Une fois que j'ai créé mon Dockerfile, je peux lancer la création de mon image docker.

docker build . -t lambda_test_tferdinand.net:v1.0

Ensuite, je n'ai plus qu'à exécuter ma fonction lambda en lançant le container avec le nom de mon handler lambda, dans mon cas : main.lambda_handler.

docker run --rm lambda_test_tferdinand.net:v1.0 main.lambda_handler

Comme vous pouvez le voir, on récupère les mêmes informations que lors d'une exécution lambda classique, avec la consommation mémoire et durée d'exécution.

De plus, on peut voir le "print" que j'avais mis dans mon script ainsi que le code retour de la fonction lambda.

Monter directement le code dans le container

Dans le cas où seul mon code évolue, mais pas mes dépendances, on peut aussi gagner un peu de temps pour les tests à venir en construisant une image docker avec les dépendances et en montant simplement le répertoire contenant notre code.

Le fichier Dockerfile

FROM lambci/lambda:python3.8
COPY ./lambda/requirements.txt .
USER root
RUN pip install -r requirements.txt

Je construis ensuite l'image de la même manière :

docker build . -t lambda_test_tferdinand.net:nocode-v1.0

Et je n'ai plus qu'a monter mon code lors de l'exécution:

docker run --rm -v ${PWD}/lambda:/var/task lambda_test_tferdinand.net:nocode-v1.0 main.lambda_handler

Comme vous pouvez le voir, le résultat est exactement le même.

Les deux méthodes sont viables, je vous laisse juge afin de choisir la plus adaptée à votre besoin.

Ajouter un layer

Lambda permet l'ajout de layers, ces derniers permettent de charger dynamiquement dans Lambda des fonctions partagées entre plusieurs fonctions.

L'image docker permet aussi d'effectuer ses tests avec des layers si besoin.

Je vais modifier mon petit script pour qu'il exploite une fonction que je vais placer dans un layer, puis le tester de la même manière.

Dans un premier temps, je vais créer mon layer dans ./layers/http_caller/main.py

#!/usr/bin/env python3
import requests

def call(url):
    r = requests.get(url)
    print('Hello from my layer!')
    print('Return code : {}'.format(r.status_code))
    if r.status_code == 200:
        return 0
    else:
        return 1

Rien d'extraordinaire, toujours un simple code de démonstration.

Ensuite, je vais modifier mon code dans lambda/main.py pour invoquer la fonction de ce layer :

#!/usr/bin/env python3
import sys

sys.path.append('/opt')
from http_caller.main import call

def lambda_handler(event, context):
    return call('https://tferdinand.net')

L'ajout du répertoire "/opt" est un prérequis car AWS Lambda positionne les layers dans ce répertoire.

A ce moment, j'ai donc l'arborescence suivante :

.
├── Dockerfile
├── lambda
│   ├── main.py
│   └── requirements.txt
└── layers
    └── http_caller
        └── main.py

Enfin, je peux exécuter mon code :

  • Soit en recréant un nouveau Dockerfile :
FROM lambci/lambda:python3.8
COPY ./lambda/ .
COPY ./layers/ /opt/
USER root
RUN pip install -r requirements.txt
  • Soit en l'exécutant directement :
docker run --rm -v ${PWD}/lambda:/var/task -v ${PWD}/layers:/opt lambda_test_tferdinand.net:nocode-v1.0 main.lambda_handler

Comme vous pouvez le voir, mon code s'est bien exécuté et a bien chargé mon layer.

Exécuter une API et récupérer le contexte

Il est aussi possible d'émuler un serveur web pour reproduire le comportement d'une API Gateway ou d'un ALB devant ma fonction lambda.

Pour effectuer ce test, je vais donc modifier mon code pour qu'il me renvoie l'event qu'il a reçu :

#!/usr/bin/env python3
def lambda_handler(event, context):
    return {
            "statusCode": 200,
            "body": event
            }

Ensuite, je vais réinvoquer ma fonction lambda, en ajoutant deux paramètres :

  • Je vais changer la variable d'environnement "DOCKER_LAMBDA_STAY_OPEN" à 1 pour indiquer de laisser la lambda en mode API, il s'agit d'une des fonctionnalités de l'image pour émuler elle même le serveur
  • Je vais ouvrir le port de la dite API et le lier à mon host.
docker run --rm -v ${PWD}/lambda:/var/task -e DOCKER_LAMBDA_STAY_OPEN=1 -p 9001:9001 lambda_test_tferdinand.net:nocode-v1.0 main.lambda_handler

Comme vous pouvez le voir, ma lambda est donc prête à être invoquée.

A noter qu'il faut l'invoquer comme documenté dans la documentation officielle "Invoke"

curl -d '{"user-agent": "curl"}' http://localhost:9001/2015-03-31/functions/function/invocations

A noter que j'ai envoyé un payload que vous pouvez voir dans le "-d" pour simuler un événement lambda.

On peut voir que ma lambda s'est bien exécutée et que mon payload est bien visible dans cette dernière.

De l'autre côté, on voit les durées d'exécution et consommations mémoire, comme habituellement.

En conclusion

L'image docker lambci/lambda permet de tester de manière rapide et efficace vos fonctions lambda.

Cela permet de pouvoir valider rapidement les exécutions durant le développement, mais peut complètement s'inscrire dans un pipeline de déploiement pour des tests d'intégration.

Je n'ai couvert qu'une infime partie des possibilités de cette image ici, et je vous invite à aller sur la page associée sur DockerHub si vous souhaitez plus d'informations!

N'hésitez pas à réagir dans les commentaires pour donner votre avis sur cette image.