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

 

Étape 1 : Créer la table DynamoDB

PourquoiOn commencercommence par DynamoDB ? Carcar 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 : DefaultCustomize 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 "Créer une fonction" (Create function)

      Sélectionnez "Créer à partir de zéro" (Author from scratch)

      Configurez la fonction :

        Nom : CreateTaskFunction Runtime : Python 3.11 (ou 3.12 si disponible) Architecture : x86_64 Permissions : Select existing “LabRole” role

         

        Cliquez sur "Créer une fonction"

        Attendez 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.