TP5 - Faire une API REST 100% serverless

Le but de ce TP est de mettre en place une API REST pour gĂ©rer des tĂąches d’une to-do liste en utilisant uniquement des services serverless proposĂ©s par AWS. Cela ne va pas ĂȘtre plus compliquĂ© pour vous (voire mĂȘme cela risque d’ĂȘtre plus facile). Comme votre application sera 100% serverless, vous n’allez plus utiliser Terraform, mais AWS SAM (Serverless Application Model). Comme Terraform, SAM est une solution IaC, et va permettre de dĂ©finir l’architecture de votre application comme du code. Cette fois-ci, ce ne sera pas du code Python, mais un simple YAML (YAML Ain’t Markup Language), un format clĂ©-valeur proche du JSON, mais oĂč l’indentation est importante.

Le but du TP est de mettre en place une API REST pour gĂ©rer des tĂąches d’une to-do liste. Pendant la sĂ©ance, vous ne ferez que la partie crĂ©ation d’une tĂąche. À vous de terminer si vous le souhaitez.

Un hello world avec SAM

Pour commencer le TP, placez-vous dans le rĂ©pertoire oĂč vous souhaitez travailler, puis exĂ©cutez la commande sam init. SĂ©lectionnez l’option AWS Quick Start Templates, puis choisissez le premier template disponible. Lorsque vous ĂȘtes invitĂ© Ă  choisir un runtime, sĂ©lectionnez Python. Refusez l’utilisation de X-ray si demandĂ©, puis donnez un nom Ă  votre projet. Une fois le projet tĂ©lĂ©chargĂ©, un dossier sera apparu avec l’arborescence suivante (certaines parties sont omises) :

📩test
 ┣ 📂events
 ┃ ┗ 📜event.json
 ┣ 📂hello_world
 ┃ ┣ 📜app.py
 ┃ ┣ 📜requirements.txt
 ┃ ┗ 📜__init__.py
 ┣ 📜template.yaml
 ┗ 📜__init__.py

Le fichier template.yaml contient l’infrastructure de votre code, le dossier hello_world va contenir le code de votre lambda (ce dossier peut ĂȘtre renommĂ©) et event.json va contenir un Ă©vĂšnement pour tester votre application.

Le fichier qui nous intéresse particuliÚrement en ce moment est le fichier template.yaml. Voici son contenu (certaines parties sont omises):

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Vous constatez qu’il y a 3 parties dans votre fichier et Ă©videmment chacune a son rĂŽle:

  • Globals permet de dĂ©finir des configurations de maniĂšre globale. Ici, le timeout des fonctions lambda du template est de 3 secondes.
  • Ressources est la partie oĂč l’infrastructure est dĂ©finie. Actuellement, ce template dĂ©finit :
    • Une fonction lambda, c’est ce que veut dire la ligne 10. Son code est dans le dossier hello-world, le handler est la fonction lambda_handler du fichier app.py, et la version de python de la fonction est 3.9.
    • Une API Gateway. C’est moins clair mais c’est ce que veut dire la ligne 19. Comme la lambda est dĂ©clenchĂ©e par une API Gateway, elle va ĂȘtre créée mĂȘme si elle n’est pas formellement dĂ©finie. Le code de la lambda sera dĂ©clenchĂ© par un appel GET sur le chemin /hello.
  • Outputs permet de rĂ©cupĂ©rer facilement des valeurs qui ne sont connues qu’une fois le template dĂ©ployĂ©. Ici l’URL pour dĂ©clencher la fonction, l’identifiant AWS de la lambda et de son rĂŽle.

Comme vous utilisez des labs AWS Academy, ce code ne fonctionne pas tel quel. En effet, ce code crĂ©e automatiquement un rĂŽle pour la lambda, sauf que c’est impossible sur un lab. Ajoutez aprĂšs Handler la ligne Role: !Sub arn:aws:iam::${AWS::AccountId}:role/LabRole pour mettre le bon rĂŽle Ă  votre lambda. Supprimez Ă©galement les 3 derniĂšres lignes du template.

Maintenant que cela est fait, il est temps de déployer le template ! Exécutez sam deploy --guided et répondez aux questions dans le terminal.

  • Stack Name : rĂ©pondez ce que vous souhaitez
  • AWS Region : us-east-1
  • Confirm changes before deploy : y
  • Allow SAM CLI IAM role creation : n
  • Capabilities [[‘CAPABILITY_IAM’]]: validez avec entrĂ©e
  • Disable rollback [y/N]: rĂ©pondez ce que vous souhaitez
  • HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
  • Validez les prompts suivants avec entrĂ©e

AprÚs quelques instants, la liste des ressources à déployer va apparaßtre dans votre terminal

Operation LogicalResourceId ResourceType Replacement
  • Add
HelloWorldFunctionHelloWorldPermissionProd AWS::Lambda::Permission N/A
  • Add
HelloWorldFunction AWS::Lambda::Function N/A
  • Add
ServerlessRestApiDeployment47fc2d5f9d AWS::ApiGateway::Deployment N/A
  • Add
ServerlessRestApiProdStage AWS::ApiGateway::Stage N/A
  • Add
ServerlessRestApi (AWS::ApiGateway::RestApi) N/A

Comme c’est un premier dĂ©ploiement, vous avez seulement des opĂ©rations Add. Votre template tout simple est en train de crĂ©er :

  • Une fonction lambda (AWS::Lambda::Function) et une politique de sĂ©curitĂ© associĂ©e (AWS::Lambda::Permission)
  • Une API Gateway de type Rest (AWS::Lambda::Function), avec un stage (AWS::ApiGateway::Stage) le tout accessible sur internet (AWS::ApiGateway::Deployment)

Validez les changements et aprĂšs quelques instants, un bloc de sortie va apparaĂźtre dans le terminal. Copiez/collez l’URL de l’API et vĂ©rifiez qu’elle fonctionne bien.

🎉 FĂ©licitations, vous venez de crĂ©er votre premier API REST 100% serverless sur AWS ! Avec un peu de travail, vous pourriez redĂ©ployer tout votre projet informatique de 2A.

Ajouter une base de données DynamoDB

DynamoDB est une base de donnĂ©es serverless, il est donc possible de l’ajouter dans le template. Dans la partie Resources du template ajoutez :

DynamoDBTable:
  Type: AWS::DynamoDB::Table
  Properties: 
    AttributeDefinitions: 
      - AttributeName: user
        AttributeType: S
      - AttributeName: id
        AttributeType: S
    KeySchema: 
      - AttributeName: user
        KeyType: HASH
      - AttributeName: id
        KeyType: RANGE
    ProvisionedThroughput: 
      ReadCapacityUnits: 5
      WriteCapacityUnits: 5

Et dĂ©ployez Ă  nouveau votre template. Vous allez voir que seule la nouvelle ressource va ĂȘtre dĂ©ployĂ©e.

Maintenant, il faut arriver Ă  faire le lien entre la base DynamoDB et la fonction lambda. Pour rendre le code le plus souple possible, le nom de la table ne va pas ĂȘtre hardcodĂ© dans la fonction, mais mis dans ses variables d’environnement. Dans les propriĂ©tĂ©s de votre fonction, ajoutez les lignes :

Environment:
  Variables:
    DYNAMO_TABLE: !Ref DynamoDBTable

Ces lignes vont faire que lors du dĂ©ploiement, une variable d’environnement DYNAMO_TABLE va ĂȘtre créée, et qu’elle va avoir pour valeur le nom de la table. Comme le nom est gĂ©nĂ©rĂ© dynamiquement, on va passer une rĂ©fĂ©rence Ă  la ressource DynamoDB (!Ref DynamoDBTable) et SAM va dynamiquement injecter le nom.

Déployez le template, allez sur la page du service AWS Lambda, puis sur la lambda créée par le template, puis configuration et Variables d'environnement et vérifiez que tout est bon.

Le code de la lambda

Pour le moment, vous avez seulement fait la partie infrastructure de l’application, il est temps de regarder un peu le code. Quand AWS SAM a gĂ©nĂ©rĂ© le template de base, il a créé un dossier hello_world. Ce dossier contient le code de la fonction lambda. Quand AWS SAM va dĂ©ployer le code de la lambda, il cherche le contenu de la variable codeUri et dĂ©ploie l’intĂ©gralitĂ© du dossier. Si le dossier contient un fichier requirements.txt, SAM va installer les dĂ©pendances pour la lambda. Si vous regardez le fichier app.py, il contient la mĂ©thode lambda_handler(), qui pour le moment ne renvoie qu’une rĂ©ponse prĂ©dĂ©finie.

  1. Dans le template.yml, changez le nom de la ressource HelloWorldFunction en PostTaskFunction (attention, vous devez mettre à jour la derniÚre ligne du template également).

  2. Changez dans les Ă©vĂ©nements de la fonction le nom de l’API (HelloWorld -> Task), et sa propriĂ©tĂ© (/hello -> task, get -> post).

  3. Créez un dossier PostTask et faites que votre lambda pointe vers ce dossier.

  4. ImplĂ©mentez la fonction lambda_handler() qui va poster un commentaire dans la base DynamoDB. Une requĂȘte HTTP POST permet de crĂ©er une ressource, les Ă©lĂ©ments de la ressource seront dans le body de la requĂȘte. Voici un exemple de body :

{
	"user" : "Rémi",
    "taskTitle" : "Corriger le TP noté",
    "taskBody" : "Tout est dans le titre",
    "taskDueDate" : "18/05/2023"
}

Pour vous aider :

  • Pour rĂ©cupĂ©rer le contenu du corps de la requĂȘte, utilisez body = json.loads(event['body']) (pensez Ă  importer le package json, ce package est dans les packages de base de Python, pas besoin de l’ajouter dans le requirements.txt). La variable body est un dictionnaire Python.

  • Chaque tĂąche aura un identifiant unique gĂ©nĂ©rĂ© avec la fonction uuid.uuid4() (il faut importer uuid, c’est un package dans la distribution Python de base).

  • Voici un exemple de code pour Ă©crire un objet dans DynamoDB. Ce code n’est pas Ă  reprendre tel quel ! Vous trouverez le nom de la table dans les variables d’environnement de votre lambda.

import boto3
# Get the service resource.
dynamodb = boto3.resource('dynamodb')
# Get the table.
table = dynamodb.Table('users')
# Put item to the table.
table.put_item(
   Item={
        'username': 'janedoe',
        'first_name': 'Jane',
        'last_name': 'Doe',
        'age': 25,
        'account_type': 'standard_user',
    }
)

đŸ§™â€â™‚ïžBien que n’étant pas par dĂ©faut dans python, boto3 est par dĂ©faut dans les lambdas

  • Une fois l’ajout fait, faites retourner Ă  votre lambda une dictionnaire s’inspirant de celui-ci :

    {
        "statusCode": 200,
        "body": votre objet task
    }