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:
-
I dati grezzi arrivano su S3.
-
I modelli girano su EC2 o SageMaker.
-
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 configurenel 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.
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).
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.2xlargeaccesa per un weekend può costarti centinaia di euro. Scrivere script di “auto-shutdown” è educazione finanziaria. Le aziende amano chi sa ottimizzare i costi.
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).
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!
Articoli di approfondimento
- 👉 Boto3 in 15 minuti: guida definitiva per principianti
- 👉 Guida Data Visualization 2025: come scegliere i grafici per la Business Intelligence
- 👉 Dashboard: trasformarla da cruscotto di dati a mappa strategica per le decisioni
- 👉 Progettare database relazionali: guida pratica, ERD e integrità dei dati
- 👉 Gestione magazzino: FIFO, LIFO, FEFO, JIT, VMI e ottimizzazione con ERP
- 👉 MRP II e ERP: guida completa, evoluzione, differenze e applicazioni pratiche
- 👉 Archivi essenziali di un sistema ERP: dati fondamentali e gestione della produzione
- 👉 La Balanced Scorecard




