Boto3 e AWS: Guida pratica Python per Data Science e DevOps (con codice)

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Guida pratica a Boto3 con esercizi
Ti sei mai chiesto cosa distingue un junior che “gioca” col cloud da un Cloud Architect o un Senior Data Scientist?

Il junior passa le giornate a cliccare pulsanti arancioni sulla console web di AWS. Carica file trascinandoli, lancia istanze virtuali a mano e, spesso, dimentica di spegnerle, trovandosi brutte sorprese in fattura a fine mese.

Il professionista, invece, scrive codice.

In un progetto reale di Data Science o Machine Learning, l’infrastruttura non è qualcosa che si configura: è qualcosa che si programma.

Tipicamente:

  1. I dati grezzi arrivano su S3.

  2. I modelli girano su EC2 o SageMaker.

  3. I metadati finiscono su DynamoDB.

Questa guida non è un elenco di comandi. È un percorso pratico per insegnarti a dialogare con AWS usando Boto3, la libreria che trasforma i servizi Amazon in oggetti Python. Simuleremo scenari reali per farti “sporcare le mani” con il vero lavoro di un Cloud Engineer.

Apri il terminale o il tuo Jupyter Notebook. Si comincia.


 Prerequisiti

  • Account AWS (Free Tier va benissimo).

  • Python installato.

  • AWS CLI configurata (aws configure nel terminale con le tue Access Keys).

  • Libreria installata: pip install boto3


Esercizio 1 – Verifica connessione e “Security Audit” dei Bucket

Scenario: Sei appena entrato in azienda. Il tuo primo compito è verificare che le tue credenziali funzionino e capire a quali dati hai accesso. Non farlo a mano: fallo con uno script che potrai riutilizzare.

import boto3
from botocore.exceptions import ClientError, NoCredentialsError
import sys

def verifica_connessione_e_lista_bucket():
    try:
        # 'client' è l'interfaccia low-level, perfetta per operazioni amministrative
        s3_client = boto3.client('s3')
        
        print("✅ Connessione a AWS riuscita!\n")
        print("I tuoi bucket S3 disponibili:")
        print("-" * 50)
        
        response = s3_client.list_buckets()
        buckets = response['Buckets']
        
        if not buckets:
            print("Nessun bucket trovato. Crea il tuo primo bucket!")
        else:
            for i, bucket in enumerate(buckets, 1):
                nome = bucket['Name']
                # Formattiamo la data per renderla leggibile
                creazione = bucket['CreationDate'].strftime('%Y-%m-%d %H:%M:%S')
                print(f"{i:2d}. {nome}  (creato il {creazione})")
                
            # Extra: Debug della regione del primo bucket
            loc = s3_client.get_bucket_location(Bucket=buckets[0]['Name'])
            region = loc['LocationConstraint'] or 'us-east-1 (default)'
            print(f"\n📍 Regione del primo bucket: {region}")
            
    except NoCredentialsError:
        print("❌ Credenziali AWS non trovate. Hai lanciato 'aws configure'?")
    except ClientError as e:
        error_code = e.response['Error']['Code']
        if error_code == 'InvalidAccessKeyId':
            print("❌ Access Key non valida.")
        elif error_code == 'SignatureDoesNotMatch':
            print("❌ Secret Key errata.")
        else:
            print(f"Errore AWS imprevisto: {e}")

if __name__ == "__main__":
    verifica_connessione_e_lista_bucket()

🔎 Analisi Didattica: Perché questo esercizio è importante?

In produzione, gli errori non sono mai generici. Questo esercizio insegna a gestire le eccezioni specifiche (ClientError).

💡 L’Opinione dell’Esperto (Security): Perché farlo via codice? Immagina di avere 500 bucket. Controllarli a mano è impossibile. Con uno script simile a questo, puoi iterare su tutti i bucket e verificare in pochi secondi quali sono “Pubblici” e quindi rischiosi. È così che nascono i tool di Security Auditing.


Esercizio 2 – Upload sicuro di un Dataset (Data Engineering)

Scenario: Hai pulito il file sales_2024.csv. Devi caricarlo nel Data Lake aziendale. Non basta “buttarlo su”: devi assicurarti che sia criptato e abbia i metadati corretti per essere letto via web o da altri tool.

import boto3
from botocore.exceptions import ClientError
import os
from pathlib import Path

def upload_dataset(file_path, bucket_name, s3_key=None, extra_args=None):
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File locale non trovato: {file_path}")
    
    s3_client = boto3.client('s3')
    s3_key = s3_key or Path(file_path).name
    
    # Best Practices per produzione:
    # 1. ACL: garantisce che il proprietario del bucket abbia il controllo completo
    # 2. Encryption: Mai salvare dati in chiaro (AES256 è lo standard)
    default_args = {
        'ACL': 'bucket-owner-full-control',
        'ServerSideEncryption': 'AES256'
    }
    if extra_args:
        default_args.update(extra_args)
    
    print(f"🚀 Upload: {file_path} → s3://{bucket_name}/{s3_key}")
    
    try:
        # upload_file gestisce automaticamente il multipart upload per file grossi
        s3_client.upload_file(
            Filename=file_path,
            Bucket=bucket_name,
            Key=s3_key,
            ExtraArgs=default_args
        )
        print(f"✅ Upload completato!")
        
    except ClientError as e:
        if e.response['Error']['Code'] == 'NoSuchBucket':
            print(f"❌ Il bucket '{bucket_name}' non esiste (i nomi sono globali!).")
        elif e.response['Error']['Code'] == 'AccessDenied':
            print("❌ Accesso Negato. Controlla le policy IAM del tuo utente.")
        else:
            raise e

# Esempio d'uso (Assicurati di creare prima un bucket vuoto e un file dummy)
if __name__ == "__main__":
    # Crea un file dummy al volo per il test
    with open("test_data.csv", "w") as f: f.write("id,valore\n1,100")
    
    upload_dataset(
        file_path="test_data.csv",
        bucket_name="inserisci-qui-il-tuo-bucket-univoco", 
        s3_key="raw/sales/2024/test_data.csv",
        extra_args={'ContentType': 'text/csv'}
    )

🔎 Analisi Didattica: L’importanza di ExtraArgs

Molti principianti usano upload_file senza argomenti. Questo crea oggetti “nudi”. Qui insegniamo a definire il ContentType (fondamentale se il file deve essere aperto da un browser o visualizzato correttamente) e la crittografia lato server (AES256), che è spesso un requisito obbligatorio nelle grandi aziende per la compliance GDPR/ISO.

Forse potrebbe interessarti anche:  Python e Excel: Come Automatizzare in 10 Minuti Analisi di Dati che Richiederebbero 4 Ore di Lavoro Manuale - Case Study su Report Vendite Multi-Store

Pubblicità


Esercizio 3 – Smart Caching: Scaricare solo se necessario

Scenario: Il tuo notebook Jupyter deve leggere un file di transazioni. Il file è grande (2GB). Scaricarlo ogni volta che riavvii il kernel ti fa perdere 10 minuti. Scriviamo una funzione che scarica il file solo se è cambiato rispetto alla versione locale.

import boto3
import hashlib
from pathlib import Path
from botocore.exceptions import ClientError

def download_if_changed(bucket, key, local_path):
    s3 = boto3.client('s3')
    local_path = Path(local_path)
    
    # 1. Calcoliamo l'hash MD5 locale (se il file esiste)
    local_etag = None
    if local_path.exists():
        hasher = hashlib.md5()
        with open(local_path, 'rb') as f:
            # Leggiamo a blocchi per non saturare la RAM
            for chunk in iter(lambda: f.read(4096), b""):
                hasher.update(chunk)
        local_etag = f'"{hasher.hexdigest()}"' # S3 formatta l'ETag tra virgolette
    
    try:
        # 2. Chiediamo a S3 i metadati SENZA scaricare il file (head_object)
        head = s3.head_object(Bucket=bucket, Key=key)
        remote_etag = head['ETag']
        
        # Nota: Per file caricati in multipart, l'ETag S3 non è un semplice MD5.
        # In quel caso servirebbe una logica di confronto più complessa.
        
        if local_etag == remote_etag:
            print("😎 File già aggiornato localmente. Risparmiato download.")
            return str(local_path)
            
        print(f"🔄 Nuova versione trovata (ETag {remote_etag}). Download...")
        local_path.parent.mkdir(parents=True, exist_ok=True)
        s3.download_file(bucket, key, str(local_path))
        print(f"✅ Download completato: {local_path}")
        return str(local_path)
        
    except ClientError as e:
        print("❌ Errore nel download (File non trovato o permessi mancanti).")
        raise

# Esempio (Usa i parametri dell'esercizio precedente)
# download_if_changed("tuo-bucket", "raw/sales/2024/test_data.csv", "cache/data.csv")

🔎 Analisi Didattica: Ottimizzazione

Questo script introduce il concetto di Idempotenza e ottimizzazione delle risorse. Il comando head_object è rapidissimo ed economico. Verificare l’ETag prima di scaricare è una best practice che riduce i tempi di avvio delle pipeline di Machine Learning e i costi di trasferimento dati (Data Transfer Out).

Forse potrebbe interessarti anche:  Prompt Engineering per AI Generativa

Esercizio 4 – FinOps: Gestione Lifecycle EC2 (Stop & Start)

Scenario: Vuoi allenare un modello su una potente istanza GPU. Vuoi lanciarla, far girare lo script e spegnerla immediatamente dopo.

import boto3
import time

ec2 = boto3.client('ec2')
INSTANCE_ID = 'i-xxxxxxxxxxxxxxxxx'  # Inserisci l'ID della tua istanza di test

def gestisci_ciclo_vita():
    print(f"Avvio istanza {INSTANCE_ID}...")
    ec2.start_instances(InstanceIds=[INSTANCE_ID])
    
    # I Waiter sono fondamentali: bloccano lo script finché lo stato non è raggiunto
    waiter = ec2.get_waiter('instance_running')
    waiter.wait(InstanceIds=[INSTANCE_ID])
    print("✅ Istanza RUNNING. Ora potresti connetterti via SSH ed eseguire il training.")
    
    # Simulazione del lavoro
    print("⏳ Esecuzione task in corso (simulato 5 sec)...")
    time.sleep(5)
    
    print("🛑 Task finito. Arresto istanza per risparmiare costi...")
    ec2.stop_instances(InstanceIds=[INSTANCE_ID])
    ec2.get_waiter('instance_stopped').wait(InstanceIds=[INSTANCE_ID])
    print("✅ Istanza STOPPED. Non stai più pagando per la compute capacity.")

# if __name__ == "__main__":
#    gestisci_ciclo_vita() 

🔎 Analisi Didattica: Automazione e Waiters

La parte critica qui sono i waiters. Senza di essi, lo script proverebe a eseguire comandi su una macchina che sta ancora facendo il boot, fallendo.

💡 Visione FinOps: Il cloud non è gratis. Dimenticare una istanza g5.2xlarge accesa per un weekend può costarti centinaia di euro. Scrivere script di “auto-shutdown” è educazione finanziaria. Le aziende amano chi sa ottimizzare i costi.

Pubblicità


Intermezzo Teorico: Capire DynamoDB prima di usarlo

Per l’ultimo esercizio useremo DynamoDB. Dimentica SQL e le tabelle rigide.

  • Schema-less: Non devi definire le colonne prima. Ogni riga (Item) può avere attributi diversi.

  • Partition Key: È l’equivalente dell’indice primario. DynamoDB la usa per sapere su quale server fisico salvare i dati. Deve essere univoca (es. ExperimentID).

  • Velocità: Progettato per latenze di millisecondi a qualsiasi scala.

  • Prezzo: Paghi per lettura/scrittura, non per avere il server acceso (nella modalità On-Demand).

Forse potrebbe interessarti anche:  Analisi della Fedeltà dei Clienti con Python: Un Approccio Pratico con Pandas

Esercizio 5 – DynamoDB come registro esperimenti ML

Scenario: Stai facendo decine di esperimenti di training. Vuoi salvare i parametri (learning rate, epoche) e i risultati (accuracy) in un posto veloce ed economico da interrogare.

import boto3
from datetime import datetime
from decimal import Decimal
import json

# Usa 'resource' per DynamoDB: è più pythonico e facile da usare del 'client'
dynamodb = boto3.resource('dynamodb')
# Assicurati di aver creato la tabella 'ML-Experiments' con Partition Key: ExperimentID (String)
table = dynamodb.Table('ML-Experiments')

def salva_esperimento(experiment_id, params, metrics):
    # Timestamp ISO 8601
    now = datetime.utcnow().isoformat()
    
    item = {
        'ExperimentID': experiment_id,
        'Timestamp': now,
        'Parameters': params,   # DynamoDB accetta dizionari annidati (Mappe)
        'Metrics': metrics,
        'Status': 'COMPLETED'
    }
    
    # Trick: DynamoDB richiede Decimal invece di float python nativi
    item_ready = json.loads(json.dumps(item), parse_float=Decimal)
    
    print(f"💾 Salvataggio esperimento {experiment_id}...")
    table.put_item(Item=item_ready)
    print("✅ Salvato.")

def leggi_esperimento(experiment_id):
    response = table.get_item(Key={'ExperimentID': experiment_id})
    if 'Item' in response:
        print(f"\n🔍 Dettagli esperimento {experiment_id}:")
        print(response['Item'])
    else:
        print("❌ Esperimento non trovato.")

# Esempio d'uso
if __name__ == "__main__":
    # Nota come Parameters e Metrics siano strutture flessibili
    salva_esperimento(
        experiment_id="exp-ResNet-042",
        params={"lr": 0.001, "batch": 64, "optimizer": "adam"},
        metrics={"accuracy": 0.945, "loss": 0.12}
    )
    
    leggi_esperimento("exp-ResNet-042")

🔎 Analisi Didattica: NoSQL per Data Science

Questo esercizio mostra la flessibilità del NoSQL. Oggi salvi accuracy, domani potresti voler salvare f1-score o recall. Con SQL dovresti fare ALTER TABLE. Con DynamoDB, aggiungi semplicemente il campo al dizionario Python e salvi. È perfetto per la natura iterativa e caotica della ricerca in Data Science.


Conclusione

Ora hai un kit di script che copre il 90% delle operazioni quotidiane su AWS. Il prossimo passo? Prendi lo script dell’Esercizio 2 e combinalo con un Loop: prova a caricare automaticamente tutti i file .csv di una cartella locale.

Buon coding!

Pubblicità