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