TP4 - Stocker des donnĂ©es dans le Cloud ⛅

đŸ§± Mise en place

Allez sur la plateforme AWS Academy et accédez au cours AWS Academy Learner Lab. Puis cliquez sur Modules > Learner Lab. Lancez votre environnement en cliquant sur Start Lab. Une fois le cercle passé au vert, cliquez sur AWS Details et AWS CLI. Les clés que vous voyez vont permettre un accÚs programmatique à votre compte. Cherchez le dossier .aws sur votre machine puis remplacez le contenu du fichier credentials par les clés que vous venez de récupérer.

đŸ§ș Manipulation de S3

CrĂ©ation d’un bucket S3 đŸ§ș

Sur la console AWS, cherchez le service S3. Normalement, vous ne devez pas avoir de bucket associĂ© Ă  votre compte. En utilisant le CDK de Terraform, crĂ©ez un bucket. La classe Ă  utiliser est la classe S3Bucket. Voici un petit bout de code pour vous aider. Attention, ce code ne fonctionne pas tel quel! Il doit ĂȘtre mis dans une classe qui hĂ©rite de TerraformStack comme dans les TP prĂ©cĂ©dents.

from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket

bucket = S3Bucket(
    self, "s3_bucket",
    bucket_prefix = "my-cdtf-test-bucket",
    force_destroy=True
)

Déployez votre architecture et vérifiez si votre bucket est bien créé.

Manipulation d’objets đŸˆđŸ©łđŸŽš

Maintenant, vous allez ajouter des objets dans votre bucket. Vous trouverez sur Moodle diffĂ©rents fichiers Ă  tĂ©lĂ©verser, mais vous pouvez utiliser les vĂŽtres si vous le souhaitez. Une fois vos fichiers tĂ©lĂ©versĂ©s, essayez de les rĂ©cupĂ©rer, les lire et les supprimer depuis Python. Voici des exemples de code pour vous aider. Attention, ces codes doivent ĂȘtre mis dans un fichier Python diffĂ©rent que celui de votre stack terraform. Il vous faut en plus installer la bibliothĂšque Python boto3 avec pip install boto3

import boto3
# Create an S3 resource
s3 = boto3.resource('s3')
# Create a bucket
s3.create_bucket(Bucket='mybucket')
# Upload file
with open('/tmp/hello.txt', 'rb') as file:
    s3.Object('mybucket', 'hello_s3.txt').put(Body=file)
# Download file
s3.Bucket('mybucket').download_file('hello_s3.txt', 'hello.txt')
# Delete file
s3 = boto3.resource('s3')
s3.Object('mybucket', 'hello_s3.txt').delete()

Ajout du versionnage 🔮🟠🟡

Un bucket S3 peut versionner ses objets et ainsi conserver les diffĂ©rentes versions d’un mĂȘme fichier. Cette fonctionnalitĂ© est utile pour ne pas perdre des donnĂ©es, mais elle va augmenter les coĂ»ts, car toutes les versions vont compter dans le volume facturĂ©. Activez le versionnage en ajoutant un attribut versioning valant {"enabled":True} Ă  l’objet S3Bucket. RedĂ©ployez votre infrastructure.

Maintenant, avec votre code Python, tĂ©lĂ©versez un fichier qui aura le mĂȘme nom qu’un objet dĂ©jĂ  prĂ©sent dans votre bucket. Allez sur la console AWS, cherchez le service S3, cliquez sur votre bucket puis sur l’objet rĂ©uploadĂ©. Dans l’onglet Version, vous devriez voir les diffĂ©rentes versions de votre objet.

đŸ§™â€â™‚ïž Une fois que l’option de versionnage est activĂ©e sur un bucket S3, elle ne peut plus ĂȘtre dĂ©sactivĂ©e, mais seulement suspendue. Cela signifie que les nouveaux objets ne seront pas versionnĂ©s, mais que les anciens garderont leurs versions.

🎆Manipulation de DynamoDB

Cet exercice s’inspire du workshop DynamoDB d’AWS : https://amazon-dynamodb-labs.com/game-player-data.html

Cette partie du TP consiste Ă  mettre en place une base de donnĂ©es pour stocker des donnĂ©es d’un jeu de type battle royal. Chaque partie regroupe 50 joueurs qui s’affrontent pendant une trentaine de minutes. Notre base devra stocker en temps rĂ©el le temps jouĂ© par chaque joueur, leur score, et quel joueur l’a emportĂ©. Chaque joueur devra pouvoir accĂ©der Ă  ses parties passĂ©es et les revisiter.

ModĂšle de donnĂ©es đŸ§©

Conceptuellement, notre jeu va mobiliser 2 concepts :

  • Les joueurs (User)
  • Les parties (Game)

Et une table pour associer les deux. Un joueur va pouvoir créer une partie (il en devient le Creator), mais il peut rejoindre une partie et créer une ligne dans la table GameUserMapping.

DynamoDB est une base de donnĂ©es NoSQL qui ne dispose pas de moteur de jointure et ne peut pas effectuer d’agrĂ©gation de type GROUP BY. En revanche, elle offre d’excellentes performances, quel que soit le volume de donnĂ©es. Choisir une base de donnĂ©es NoSQL plutĂŽt qu’une base SQL est un choix qui entraĂźne des diffĂ©rences significatives dans les outils disponibles.

Dans notre cas, avec trois entitĂ©s, il n’est pas nĂ©cessaire de crĂ©er plusieurs tables. À la place, nous allons utiliser une seule grande table dans cet exercice qui contiendra toutes les donnĂ©es. Cependant, nous devons pouvoir identifier de maniĂšre unique les diffĂ©rentes informations de notre base, Ă  savoir User, Game et GameUserMapping. Un utilisateur est identifiĂ© de maniĂšre unique par son USERNAME, un jeu par son GAME_ID et une ligne de GameUserMapping par le couple GAME_ID et USERNAME.

Une table DynamoDB est dĂ©finie par une clĂ© primaire, qui peut ĂȘtre soit sa clĂ© de partition (hash key), soit le couple clĂ© de partition, clĂ© de tri (sort key). Au vu de notre modĂšle de donnĂ©es (association many-to-many), la meilleure solution est d’utiliser une clĂ© composite. Ainsi, la clĂ© de notre table DynamoDB aura la forme suivante :

Entity Partition Key Sort Key
User USER# #METADATA#
Game GAME# #METADATA#
UserGameMapping GAME# USER#

Pour rappel, nous allons utiliser une seule table, mais les lignes pourront reprĂ©senter plusieurs concepts en fonction de leur combinaison de clĂ© de partition/clĂ© de tri. Pour Ă©viter toute confusion entre les USERNAME et les GAME_ID, nous allons prĂ©fixer ces valeurs par le concept qu’elles reprĂ©sentent, comme l’entitĂ© UserGameMapping. L’avantage de cette approche est que nous pourrons ajouter des index secondaires Ă  notre table pour effectuer des requĂȘtes complexes, ce qui serait impossible avec plusieurs tables distinctes. Cependant, contrairement Ă  un modĂšle relationnel qui peut rĂ©pondre Ă  presque toutes les questions avec une seule requĂȘte, ici, il est essentiel de connaĂźtre les questions que l’on souhaite poser Ă  la base de donnĂ©es et de la concevoir en consĂ©quence. La phase d’analyse des besoins est donc particuliĂšrement importante !

CrĂ©ation et peuplement d’une table 🎼

CrĂ©ez une table DynamoDB en utilisant le CDK Terraform. Votre table s’appellera battle-royale et aura comme partition key la clef PK qui sera un String, et la sort key SK qui sera une String aussi. Voici un code d’aide pour crĂ©er votre table

 from cdktf_cdktf_provider_aws.dynamodb_table import DynamodbTable, DynamodbTableAttribute

 
bucket = DynamodbTable(
    self, "DynamoDB-table",
    name= "user_score",
    hash_key="username",
    range_key="lastename",
    attribute=[
        DynamodbTableAttribute(name="username",type="S" ),
        DynamodbTableAttribute(name="lastename",type="S" )
    ],
    billing_mode="PROVISIONED",
    read_capacity=5,
    write_capacity=5
)

đŸ§™â€â™‚ïž Les trois derniers paramĂštres sont liĂ©s Ă  la facturation de votre table. Laissez-les tels quels.

Une fois la table créée, crĂ©ez un script Python “classique” (= pas liĂ© Ă  Terraform), et chargez les donnĂ©es contenues dans le fichier items.json. Chaque ligne de ce fichier est un JSON qui contient une ligne de notre table. Comme il y a beaucoup de donnĂ©es, faites un envoi en batch. Voici des codes pour vous aider. L’idĂ©e est d’ouvrir le fichier et un batch_writer, et quand vous lisez une ligne vous l’ajoutez au batch_writer.

# Read file
import json
with open('myfile.json', 'r') as f:
    for row in f:
        items.append(json.loads(row))
 
# Batch upload
import boto3
# Get the service resource.
dynamodb = boto3.resource('dynamodb')
# Get the table.
table = dynamodb.Table('users')
# Batch writing item. Only one big query, cost less ans it's quicker
with table.batch_writer() as batch:
    for i in range(50):
        batch.put_item(
            Item={
                'account_type': 'anonymous',
                'username': 'user' + str(i),
                'first_name': 'unknown',
                'last_name': 'unknown'
            }
        )

Si tout a l’air de s’ĂȘtre bien passĂ©, requĂȘtez la table pour compter le nombre de lignes. Voici le code Ă  exĂ©cuter :

# Get the service resource.
dynamodb = boto3.resource('dynamodb')
# Get the table.
table = dynamodb.Table('battle-royal')

response = table.scan(
    Select='COUNT',
    ReturnConsumedCapacity='TOTAL',
)

Vous devrez obtenir 835 lignes et 14.5 capacityUnits d’utilisĂ©e.

Lire les donnĂ©es 👀

RĂ©cupĂ©rer les donnĂ©es d’un joueur

Récupérez les données associées au joueur avec le username johnsonscott. Voici un code pour vous aider

import boto3
# Get the service resource.
dynamodb = boto3.resource('dynamodb')
# Get the table.
table = dynamodb.Table('item')
item="apple_pie"
resp = table.query(
    Select='ALL_ATTRIBUTES',
    KeyConditionExpression="PK = :pk AND SK = :name",
    ExpressionAttributeValues={
        ":pk": f"PRODUCT#{item}",
        ":name": f"#NAME#{item}",
    },
)

Récupérer les informations sur une partie

En vous inspirant du code précédent, récupérez les informations correspondantes à la partie : c9c3917e-30f3-4ba4-82c4-2e9a0e4d1cfd.

Récupérer la liste des joueurs pour une partie

Si vous regardez plus en dĂ©tails le contenu de la clĂ© Items du rĂ©sultat prĂ©cĂ©dent, vous remarquerez que vous avez rĂ©cupĂ©rĂ© une ligne liĂ©e de l’entitĂ© Game et 50 autres de l’entitĂ© UserGameMapping. RĂ©alisez une requĂȘte qui ne vous retournera que les joueurs d’une partie donnĂ©e. Pour ce faire, vous pouvez utiliser la condition begins_with(col, val) dans la condition de votre requĂȘte.

Ajout d’index secondaires đŸ„ˆ

Les index secondaires sont une fonctionnalitĂ© importante de DynamoDB. Ils permettent de dĂ©finir une nouvelle clĂ© primaire, ce qui permet de requĂȘter la table diffĂ©remment. Chaque index secondaire doit permettre de rĂ©aliser de nouveaux types de requĂȘtes et doit ĂȘtre placĂ© judicieusement. En d’autres termes, si vous avez besoin d’index secondaires, crĂ©ez-en, sinon vous pouvez vous en dispenser !

Index inversé

Actuellement, notre base nous permet Ă  partir d’une partie de rĂ©cupĂ©rer la liste des joueurs, mais pas l’historique des parties d’un joueur. Cela provient du choix de la partition key et de la sort key pour UserGameMapping. Comme la partition key est GAME#<GAME_ID>, on ne peut faire une recherche qu’à partir d’une partie. Pour permettre la recherche dans les deux sens, nous allons mettre en place un index inversĂ©, qui nous permettra de chercher sur la sort key.

Ajoutez l’attribut suivant Ă  l’objet DynamodbTable dans votre code CDK pour crĂ©er un index global dans votre table.

global_secondary_index=[
    DynamodbTableGlobalSecondaryIndex(
        name="InvertedIndex",
        hash_key="SK",
        range_key="PK",
        projection_type="ALL",
        write_capacity=5,
        read_capacity=5
    )
]

Une fois l’index créé, implĂ©mentez le code pour rĂ©cupĂ©rer la liste des parties jouĂ©es par un joueur. Ce code va utiliser la mĂ©thode query, mais vous allez devoir ajouter un paramĂštre IndexName avec le nom de l’index pour rĂ©aliser votre requĂȘte.

Index secondaire “creux”

Il est Ă©galement possible de poser un index sur un attribut de la table. Cela permettra de faire des requĂȘtes sur cet attribut comme s’il Ă©tait une clĂ© primaire. Il n’y a pas besoin que l’attribut en question soit prĂ©sent dans tous les Ă©lĂ©ments de la table. Seuls les Ă©lĂ©ments avec l’attribut utilisĂ© seront indexĂ©s (d’oĂč le nom d’index creux)

Poser un tel index permet de faire de nouvelles requĂȘtes, impossible Ă  faire prĂ©cĂ©demment. Par exemple, il est actuellement difficile de faire une recherche pour obtenir les parties encore ouverte sur une carte donnĂ©e. Il nous faudrait rĂ©cupĂ©rer toutes les parties, puis filtrer sur les parties avec un open_timestamp. Sauf que DynamoDB va devoir scanner toute la table, ce qui va faire exploser les coĂ»ts. La solution est de crĂ©er un index secondaire avec comme hash key map et sort key open_timestamp.

En vous inspirant du code précédent mettez en place ce nouvel index et cherchez les parties ouvertes sur la carte Green Grasslands