Skip to main content

Serverless sur AWS

Objectifs Introduction

Deux parties : 

    Pratique de la console AWS (ici) Automatisation d’Infrastructure as Code avec SAM
    objectifs globaux
      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.