AWS Lambda e DocumentDB con Python
Sperimentando un po’ con il mondo AWS insieme a Michele Scarimbolo, amico e collega -denominato Mr. AWS-, abbiamo messo mano al servizio Lambda e DocumentDB, per creare delle funzioni che fossero in grado di gestire delle operazioni CRUD sfruttando questo database documentale.
E tu che aspetti? Partiamo subito con questo tutorial su AWS Lambda e DocumentDB con Python!
Intro
AWS Lambda consente di eseguire codice senza dover effettuare il provisioning né di dover gestire alcun server.
Il servizio esegue codice su un’infrastruttura di calcolo ad alta disponibilità e amministra le risorse di elaborazione, tra cui la manutenzione del server e del sistema operativo, il provisioning e il dimensionamento automatico della capacità, il monitoraggio di codici e la creazione di log.
È sufficiente fornire il codice in uno dei linguaggi supportati da AWS Lambda (attualmente Node.js, Java, C#, Python, Go, Ruby e PowerShell) per eseguirlo!
È perfetto per l’esecuzione di porzioni di codice e funzioni atomiche o che fanno parte di un’architettura a microservizi che possono essere eseguite in poco tempo e il cui codice è stato ottimizzato.
AWS DocumentDB è un servizio di database documentale che viene normalmente associato a MongoDB come tipologia di gestione dei dati, anche se, come la stessa community di MongoDB afferma, questo è compatibile rispetto all’uso delle API solo al 34%.
Un database basato su documenti archivia in modo nativo dei dati in formato JSON; in questo caso, DocumentDB fornisce la possibilità di ricerche di documenti singoli, query tramite espressioni regolari e aggregazioni, creando un cluster di una o più istanze nelle zone di disponibilità specificate.
Quando crei un cluster utilizzando la console Amazon DocumentDB e scegli di creare una replica in una zona di disponibilità diversa, Amazon DocumentDB crea due istanze: l’istanza primaria sarà in una zona di disponibilità e l’istanza di replica in una zona di disponibilità diversa.
Creazione del cluster con DocumentDB
Prima di tutto, andiamo ad avviare un cluster su DocumentDB: per iniziare, è possibile configurare come classe quella più piccola disponibile, che in questo caso è la db.t3.medium:
Configurazione del cluster DocumentDB
Andiamo quindi a definire un utente e una password per il master seguendo le indicazioni fornite nel tab dedicato all’Authentication:
Autenticazione a DocumentDB
Nella sezione relativa alle impostazioni avanzate ci sono molte opzioni riguardo la configurazione di un backup, la gestione dei log, la configurazione di rete e via dicendo.
Non dimentichiamo uno step fondamentale: Lambda avrà bisogno di accedere al database DocumentDB, e per farlo avrà bisogno di una policy che ne permette la scrittura e la lettura.
Per impostazione predefinita, un cluster Amazon DocumentDB accetta solo connessioni sicure utilizzando Transport Layer Security (TLS). Per potersi connettere tramite TLS, sarà prima scaricare la chiave pubblica per Amazon DocumentDB.
La connessione DocumentDB non può essere stabilita perché la nostra funzione Lambda e il cluster DocumentDB devono risiedere nello stesso VPC.
Per farlo, nelle configurazioni avanzate del cluster, scorriamo verso il basso fino alla sezione Network settings e selezioniamo il VPC corretto, le sue subnet e un gruppo di sicurezza adeguato (aka security group).
Configurazione della rete di DocumentDB
Una volta che il cluster sarà creato e istanziato, ci vorranno alcuni minuti prima che nell’elenco dei cluster presenti su DocumentDB riusciremo a vedere che il cluster è disponibile:
Stato delle istanze del cluster DocumentDB
A questo punto, non ci resta che prendere la stringa di connessione al cluster e utilizzarla all’interno delle lambda per collegarci!
Per recuperare le informazioni relative alla connessione, clicchiamo sul cluster di nostro interesse e andiamo nella sezione Connectivity & Security: troveremo alcuni esempi per potersi connettere all’istanza di DocumentDB con una stringa del tipo:
NOMECLUSTER-cluster.cluster-IDCLUSTER.eu-west-1.docdb.amazonaws.com:27017
La copiamo, e ce la mettiamo da parte. Prossimo step: creazione delle Lambda per aggiungere, modificare, eliminare e recuperare un documento dal database!
Definizione delle funzioni Lambda
Il servizio Lambda ci permette di creare delle funzioni ad hoc per svolgere tutte le operazioni che vogliamo: il vero vantaggio in questo caso è la fruizione delle API che ci permettono di collegarci al cluster di DocumentDB per poter gestire i nostri dati.
Creazione di una funzione Lambda
In questo caso, andiamo quindi a creare una Lambda da zero, specificando qual è il linguaggio da utilizzare.
Adesso, selezioniamo Python: andremo a creare delle funzioni lambda che ci permettano di eseguire delle operazioni come inserimento, ricerca o cancellazione sul database di DocumentDB.
Per farlo, andremo ad utilizzare alcune librerie, tra cui_json, bson_ e pymongo: queste ci permetteranno di creare documenti, recuperarli e soprattutto restituirli tramite una risposta JSON che, per esempio, potremmo fornire tramite un’API Gateway.
Attenzione: queste dovranno essere importate nella Lambda utilizzando i livelli.
Per approfondire, leggi questo articolo su Lambda e API Gateway!
Per poter utilizzare delle librerie aggiuntive, all’interno del servizio Lambda, basterà selezionare la voce nel menù laterale chiamata Livelli e crearne uno all’interno del quale andremo a caricare uno zip con tutti i file delle librerie di cui abbiamo bisogno.
Come farlo? Per creare uno zip con le librerie, sarà sufficiente scaricarle utilizzando pip, specificando la cartella di destinazione dove andarle a salvare:
pip install pymongo -t /dest/folder
Creazione di un livello per AWS Lambda
Partiamo dall’operazione di creazione: vediamo come creare un documento e inserirlo all’interno di DocumentDB. Per farlo, importiamo le librerie necessarie e poi specifichiamo all’interno della funzione pymongo.MongoClient l’indirizzo del cluster che abbiamo creato in precedenza insieme alle credenziali per accedervi:
import json
import pymongo
from bson import json_util
def lambda_handler(event, context):
client = pymongo.MongoClient('mongodb://[USERNAME]:[PASSWORD]@[CLUSTER_NAME].XXX.eu-west-1.docdb.amazonaws.com:27017')
A questo punto, richiamiamo il database che intendiamo creare (in questo caso, my_db) e la collezione che vogliamo aggiungere (ossia users). Dal momento che il contenuto del documento sarà popolato dal body della request che arriva dall’API Gateway, andiamo a verificare che event, ossia l’oggetto con cui AWS ci permette di fornire dei parametri, contenga un body.
Nota a margine: event normalmente ha una struttura di questo tipo:
{
"resource": "/",
"path": "/",
"httpMethod": "GET",
"requestContext": {
"resourcePath": "/",
"httpMethod": "GET",
"path": "/Prod/",
...
},
"headers": {
"accept": "text/html",
"accept-encoding": "gzip, deflate, br",
"Host": "xxx.us-east-2.amazonaws.com",
"User-Agent": "Mozilla/5.0",
...
},
"multiValueHeaders": {
"accept": [
"text/html"
],
"accept-encoding": [
"gzip, deflate, br"
],
...
},
"queryStringParameters": {
"postcode": 12345
},
"multiValueQueryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"body": null,
"isBase64Encoded": false
}
Non vi fate ingannare infatti dai test, all’interno dei quali potrete inserire qualunque tipo di JSON: nel momento in cui questa Lambda verrà sfruttata da un’API Gateway, tutto ciò che arriverà dalle request, si troverà nel body.
db = client.my_db
coll = db.users
if event and 'body' in event:
body = json.loads(event['body'])
Dopo aver quindi verificato che il body non sia vuoto, andiamo a inserire il documento nel database, con questa semplice istruzione:
coll.insert_one(body)
Per verificare che sia andato a buon fine, proviamo a cercare quello stesso oggetto all’interno del database e, se presente, ritorniamo un messaggio di successo come risposta alla chiamata della funzione Lambda:
res = coll.find_one(body)
client.close()
if res:
return {
'statusCode': 200,
'body': json_util.dumps(body),
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST'
}
}
else:
return {
'statusCode': 400,
'body': json.dumps('Request NOT inserted!'),
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST'
}
}
Attenzione a due fattori: in primis, nel body di risposta viene utilizzato json_util.dumps perché insieme al metodo_loads_ questo ci consente di convertire un oggetto da BSON a JSON o viceversa: in questo caso, utilizziamo dumps perché trasforma un oggetto da BSON a JSON.
L’altra cosa a cui prestare attenzione è l’uso degli headers: qualora le Lambda che state creando si vadano ad integrare con un’API Gateway che ha le CORS policy attivate e questa sarà pubblicata su Internet, sarà necessario specificarlo nella risposta.
Per recuperare invece l’elenco completo dei documenti inseriti, la Lambda avrà le seguenti istruzioni: il codice è molto simile a quanto visto per la creazione, ma in questo caso andiamo ad utilizzare il metodo find(): specificando list() all’esterno della chiamata, questo convertirà automaticamente il risultato della query in una lista che potremo andare a restituire come risposta.
import json
import pymongo
from bson import json_util
def lambda_handler(event, context):
client = pymongo.MongoClient('mongodb://[USERNAME]:[PASSWORD]@[CLUSTER_NAME].XXX.eu-west-1.docdb.amazonaws.com:27017')
db = client.my_db
coll = db.users
res = list(coll.find())
client.close()
if len(res) >= 0:
return {
'statusCode': 200,
'body': json_util.dumps(res),
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET'
}
}
else:
return {
'statusCode': 400,
'body': json.dumps('Component NOT retrieved!')
}
Per recuperare un singolo documento, magari passando dei parametri tramite l’evento, è possibile usare il metodo find_one():
res = coll.find_one(component)
Infine, creiamo una Lambda per cancellare un documento, con il seguente codice: oltre a collegarci al database e recuperare l’ID dell’oggetto da cancellare dal body dell’evento, andiamo a verificare che questo sia presente nel database: se il risultato della ricerca non è vuoto, procediamo con la cancellazione tramite il metodo delete_one.
import json
import pymongo
from bson.objectid import ObjectId
def lambda_handler(event, context):
client = pymongo.MongoClient('mongodb://[USERNAME]:[PASSWORD]@[CLUSTER_NAME].XXX.eu-west-1.docdb.amazonaws.com:27017')
db = client.my_db
coll = db.users
if event and 'body' in event:
body = json.loads(event['body'])
user_id= body['user_id']
res = coll.find_one({"_id": ObjectId(user_id)})
if res is not None:
coll.delete_one({'_id': ObjectId(user_id)})
client.close()
return {
'statusCode': 200,
'body': json.dumps('Document deleted successfully!'),
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET'
}
}
else:
return {
'statusCode': 200,
'body': json.dumps('User with this ID does not exist!'),
'headers': {
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET'
}
}
Niente male, no? Non ti resta che collegarlo ad un’API Gateway e il gioco è fatto!