Skip to main content

Serverless sur AWS

Objectifs 

• Créer des fonctions Lambda
• Déployer des API REST avec API Gateway
• Gérer la persistance avec DynamoDB
• Automatiser les déploiements avec AWS SAM
• Comprendre les concepts fondamentaux du serverless

glossaire

● Serverless: Modèle d'exécution où le cloud gère l'infrastructure
● Lambda: Service AWS de compute serverless (fonctions as a service)
● API Gateway: Service AWS pour créer et gérer des API
● DynamoDB: Base de données NoSQL serverless d'AWS
● SAM: Serverless Application Model - framework IaC pour serverless
● CloudFormation: Service AWS d'infrastructure as code
● Cold Start: Délai au premier démarrage d'une fonction Lambda
● Warm Start: Exécution d'une fonction Lambda déjà initialisée
● IAM: Identity and Access Management - gestion des permissions AWS
● CORS: Cross-Origin Resource Sharing - mécanisme de sécurité web

Partie 1 : pratique de la console AWS

Objectif de cette partie

Créer une application TODO complète en utilisant l'interface web AWS Console. Cette approche vous permet de comprendre visuellement chaque composant avant d'automatiser avec du code.

Prérequis

● Compte AWS actif (free tier suffit)
● Accès à la console AWS
● Navigateur web moderne
● Postman ou curl pour tester l'API

Créer la table DynamoDB

On commence par DynamoDB car nos fonctions Lambda auront besoin de la table. Il faut toujours créer les dépendances avant les ressources qui les utilisent

Se connecter à la console AWS

Dans la barre de recherche en haut, tapez "DynamoDB" et cliquez sur le service

Cliquez sur le bouton orange "Créer une table" (Create table)

Configurez la table avec ces paramètres :

Nom de la table : Tasks

Clé de partition : taskId (Type: String)

Settings :

  • Table settings : Customize settings
  • Read/write capacity settings : On-demand → parfait pour le développement, pas besoin de provisionner de capacité
  • Laissez tous les autres paramètres par défaut

Cliquez sur "Créer une table" (Create table) en bas de page

Attendez que le statut passe à "Active" (environ 30 secondes)


Vérification avec un test de validation : 

Dans l'onglet Explore items, cliquez sur "Create item". Créez un item de test : taskId="test-001", title="Ma première tâche". Si l'item apparaît dans la liste, votre table fonctionne ! Vous pouvez le supprimer ensuite.

Créer la première fonction Lambda (Create Task)

Nous allons créer une fonction Lambda pour créer des tâches dans DynamoDB

Dans la console AWS, recherchez et ouvrez "Lambda"

Cliquez sur Create function

Sélectionnez Author from scratch

Configurez la fonction :

  • Nom : CreateTaskFunction
  • Runtime : Python 3.xx
  • Architecture : x86_64
  • Permissions > change default execution role :  LabRole

create function puis attendre que la fonction soit créée (quelques secondes)

 

Dans l'éditeur de code qui apparaît, remplacez tout le code par 

import json
import boto3
import uuid
from datetime import datetime

# Initialisation du client DynamoDB
# boto3 est préinstallé dans Lambda, pas besoin de l'installer
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """
    Fonction Lambda pour créer une nouvelle tâche
    
    Cette fonction :
    1. Extrait les données du corps de la requête HTTP
    2. Génère un ID unique pour la tâche
    3. Enregistre la tâche dans DynamoDB
    4. Retourne la tâche créée avec headers CORS
    
    Args:
        event: Événement API Gateway contenant body, headers, etc.
        context: Contexte d'exécution Lambda (non utilisé ici)
    
    Returns:
        dict: Réponse HTTP avec statusCode, headers et body JSON
    """
    
    try:
  # 1. Parser le corps de la requête JSON
        # API Gateway envoie le body comme string, il faut le parser
        body = json.loads(event.get('body', '{}'))
        
        # 2. Extraire et valider les champs requis
        title = body.get('title')
        description = body.get('description', '')
        
        # Validation : le titre est obligatoire
        if not title:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'  # CORS
                },
                'body': json.dumps({
                    'error': 'Le champ title est obligatoire'
                })
            }
        
        # 3. Créer l'objet tâche avec les métadonnées
        task = {
            'taskId': str(uuid.uuid4()),  # ID unique généré
            'title': title,
            'description': description,
            'status': 'pending',  # Statut par défaut
            'createdAt': datetime.now().isoformat(),  # Timestamp ISO
            'updatedAt': datetime.now().isoformat()
        }
        
        # 4. Enregistrer dans DynamoDB
        table.put_item(Item=task)
        
        # 5. Retourner la réponse de succès
        return {
            'statusCode': 201,  # 201 = Created
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'  # Permet CORS
            },
            'body': json.dumps({
                'message': 'Tâche créée avec succès',
                'task': task
            })
        }
        
    except Exception as e:
        # Gestion des erreurs
        print(f"Erreur: {str(e)}")  # Log dans CloudWatch
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'error': 'Erreur serveur interne',
                'details': str(e)
            })
        }

Configuration des permissions DynamoDB

Cliquez sur "Deploy" (bouton orange en haut) pour sauvegarder le code

Dans l’environnement AWS “test”, vous n’avez pas la main sur la configuration des droits. Vous sélectionnerez le paramétrage suivant. Les lignes suivantes sont à but pédagogique

  • Cliquez sur l'onglet "Configuration"
  • Dans le menu de gauche, cliquez sur "Permissions"
  • Cliquez sur le nom du rôle (lien bleu, quelque chose comme CreateTaskFunction-role-xxxxx) 
  • Une nouvelle page IAM s'ouvre : cliquez sur "Ajouter des permissions" > "Attacher des stratégies"
  • Dans la barre de recherche, tapez "DynamoDB" et cochez "AmazonDynamoDBFullAccess"
  • Cliquez sur "Ajouter des permissions"

En production, utilisez des permissions plus restreintes ! AmazonDynamoDBFullAccess donne accès à toutes les tables. Pour ce TP, c'est acceptable, mais en prod, créez une policy qui ne donne accès qu'à la table Tasks.

Validation : 

Cliquez sur l'onglet "Test"
Cliquez sur "Créer un nouvel événement de test"
Nommez-le "CreateTaskTest" et collez ce JSON :

{
  "body": "{\"title\": \"Apprendre Lambda\", \"description\": \"Comprendre les fonctions serverless\"}"
}

Cliquez sur "Tester"
Vous devriez voir "Execution result: succeeded" en vert
Dans la section "Response", vérifiez que statusCode est 201
Allez dans DynamoDB > Tables > Tasks > Explorer items

Vous devriez voir votre tâche créée !

Créer les autres fonctions Lambda

Créez 4 autres fonctions Lambda en suivant le même processus que l'étape 2. 

Fonction 2 : GetTasksFunction (Lister toutes les tâches)

import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Liste toutes les tâches"""
    try:
        response = table.scan()
        tasks = response.get('Items', [])
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'tasks': tasks,
                'count': len(tasks)
            })
        }
    except Exception as e:
        print(f"Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': 'Erreur serveur interne'})
        }

Fonction 3 : GetTaskFunction (Récupérer une tâche par ID)

import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Récupère une tâche spécifique par son ID"""
    try:
        task_id = event.get('pathParameters', {}).get('taskId')
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'taskId manquant'})
            }
        
        response = table.get_item(Key={'taskId': task_id})
        
        if 'Item' not in response:
            return {
                'statusCode': 404,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'Tâche non trouvée'})
            }
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'task': response['Item']})
        }
    except Exception as e:
        print(f"Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': 'Erreur serveur interne'})
        }

Fonction 4 : UpdateTaskFunction

import json
import boto3
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Met à jour une tâche existante"""
    try:
        task_id = event.get('pathParameters', {}).get('taskId')
        body = json.loads(event.get('body', '{}'))
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'taskId manquant'})
            }
        
        # Construction de l'expression de mise à jour
        update_expression = "SET updatedAt = :updatedAt"
        expression_values = {':updatedAt': datetime.now().isoformat()}
        expression_names = {}
        
        if 'title' in body:
            update_expression += ", title = :title"
            expression_values[':title'] = body['title']
        
        if 'description' in body:
            update_expression += ", description = :description"
            expression_values[':description'] = body['description']
        
        if 'status' in body:
            update_expression += ", #status = :status"
            expression_values[':status'] = body['status']
            expression_names['#status'] = 'status'
        
        update_params = {
            'Key': {'taskId': task_id},
            'UpdateExpression': update_expression,
            'ExpressionAttributeValues': expression_values,
            'ReturnValues': 'ALL_NEW'
        }
        
        if expression_names:
            update_params['ExpressionAttributeNames'] = expression_names
        
        response = table.update_item(**update_params)
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'message': 'Tâche mise à jour',
                'task': response['Attributes']
            })
        }
    except Exception as e:
        print(f"Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': 'Erreur serveur interne'})
        }

Fonction 5 : DeleteTaskFunction

import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Supprime une tâche de la base de données"""
    try:
        task_id = event.get('pathParameters', {}).get('taskId')
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'taskId manquant'})
            }
        
        table.delete_item(Key={'taskId': task_id})
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'message': 'Tâche supprimée avec succès'
            })
        }
    except Exception as e:
        print(f"Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': 'Erreur serveur interne'})
        }

À ce stade, vous devriez avoir 5 fonctions Lambda créées : CreateTaskFunction, GetTasksFunction, GetTaskFunction, UpdateTaskFunction, DeleteTaskFunction

Créer l'API Gateway

Maintenant que nos fonctions Lambda sont prêtes, créons l'API REST qui les exposera.

Création de l'API

Dans la console AWS, recherchez et ouvrez "API Gateway"

Cliquez sur "Créer une API"

Trouvez "REST API" (pas "REST API privée") et cliquez sur "Créer"

Configurez l'API :

  • Protocole : REST
  • Nouvelle API : New API
  • Nom de l'API : TasksAPI
  • Description : API REST pour gérer des tâches
  • Type de point de terminaison : Regional

Cliquez sur "Créer une API"

Configuration des routes et méthodes 

Nous allons créer la structure suivante :

/tasks
├── GET     → GetTasksFunction (lister toutes les tâches)
├── POST    → CreateTaskFunction (créer une tâche)
└── /{taskId}
    ├── GET    → GetTaskFunction (récupérer une tâche)
    ├── PUT    → UpdateTaskFunction (mettre à jour une tâche)
    └── DELETE → DeleteTaskFunction (supprimer une tâche)

Créer la ressource /tasks 

Cliquez sur  "Créer une ressource"

Nom de ressource : tasks

Chemin de ressource : /tasks “/”

Cochez "Activer CORS pour l'API Gateway"

Cliquez sur "Créer une ressource"

Ajouter la méthode POST sur /tasks

Sélectionnez la ressource "/tasks" dans l'arbre

Cliquez sur "Créer une méthode"

Sélectionnez "POST" dans la liste déroulante

Configurez l'intégration :

  • Type d'intégration : Fonction Lambda
  • Utiliser l'intégration proxy Lambda : ☑ Coché (IMPORTANT !)
  • Région Lambda : votre région (ex: us-east-1)
  • Fonction Lambda : CreateTaskFunction
  • Cliquez sur "Enregistrer"
  • Une popup demande la permission : cliquez sur "OK"

Cliquez sur "Enregistrer"

Une popup demande la permission : cliquez sur "OK"

L'intégration proxy est ESSENTIELLE ! Elle transfère directement l'événement complet à Lambda (body, headers, params, etc.). Sans proxy, vous devriez configurer manuellement les mapping templates. Toujours cocher cette option !

Ajouter les autres méthodes

Répétez le processus pour GET sur /tasks → GetTasksFunction

Ensuite, créez la ressource /{taskId} :

Sélectionnez la ressource "/tasks"

Créer une ressource

Nom : taskId

Chemin : {taskId} (avec les accolades !)

Cochez "Activer CORS pour l'API Gateway"

Créer

Créez les 3 méthodes suivantes sur /{taskId} :

  • GET → GetTaskFunction
  • PUT → UpdateTaskFunction
  • DELETE → DeleteTaskFunction

Déployer l'API

Cliquez sur "Déployer l'API"

Étape de déploiement : [Nouvelle étape]

Nom de l'étape : prod

Description : Production stage

Cliquez sur "Déployer"

Validation 

Après le déploiement, vous verrez l'URL d'invocation

URL d'invocation : https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod

Vos endpoints sont :
POST   https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/tasks
GET    https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/tasks
GET    https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/tasks/{taskId}
PUT    https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/tasks/{taskId}
DELETE https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/tasks/{taskId}

Copiez cette URL, vous en aurez besoin pour les tests !

Tester l'API complète

Nous allons tester tous les endpoints avec curl ou Postman

Test 1 : Créer une tâche (POST)

curl -X POST https://VOTRE_URL.execute-api.us-east-1.amazonaws.com/prod/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Acheter du lait",
    "description": "Lait demi-écrémé au supermarché"
  }'

Résultat attendu : 201 Created avec l'objet task contenant un taskId

Test 2 : Lister toutes les tâches (GET)

curl https://VOTRE_URL.execute-api.us-east-1.amazonaws.com/prod/tasks

Résultat attendu : 200 OK avec un tableau tasks

Test 3 : Mettre à jour une tâche (PUT)

curl -X PUT https://VOTRE_URL.execute-api.us-east-1.amazonaws.com/prod/tasks/TASK_ID \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'

Résultat attendu : 200 OK avec la tâche mise à jour

Si tous vos tests passent, votre application serverless fonctionne parfaitement ! Vous avez créé une API REST complète avec Lambda, API Gateway et DynamoDB. Passons maintenant à l'automatisation avec SAM.