Módulo 5: AWS Services for AI (S3, Lambda, SageMaker Basics)
2. S3 para AI Assets
Descripción
En esta cápsula vas a dominar S3 como data layer de tu sistema AI. S3 no es "almacenamiento genérico" — es donde viven los assets que hacen funcionar tu sistema: modelos serializados, archivos de embeddings pre-computados, documentos para RAG, prompt templates versionados, y logs de inferencia. Aprenderás a organizar un bucket para proyectos AI, a ejecutar operaciones con boto3 (put_object, get_object, list_objects_v2, presigned URLs), y a diseñar patrones de almacenamiento que escalen.
Contexto: En el Módulo 4 creaste buckets en LocalStack y subiste archivos de prueba. Ahora el foco cambia de "cómo usar S3" a "cómo usar S3 para AI." Cada operación que aprendas aquí tiene un propósito AI-specific: no subes un .txt cualquiera, subes un prompt template que tu Lambda leerá para construir inferencia. Al terminar, tendrás un módulo Python reutilizable para gestionar todos los assets de AI de tu sistema.
S3 Concepts para AI Engineers
Buckets, objetos, y prefijos
S3 Bucket: ai-assets-123456789012
├── prompts/ ← Prefijo (no es una "carpeta" real)
│ ├── summarizer/v1/system.txt ← Objeto (prompt template)
│ ├── summarizer/v2/system.txt
│ └── classifier/v1/system.txt
├── documents/ ← RAG knowledge base
│ ├── product-docs/manual-v3.pdf
│ └── faq/faq-2026-q1.json
├── embeddings/ ← Vectores pre-computados
│ ├── product-docs/embeddings.npy
│ └── faq/embeddings.npy
├── models/ ← Modelos serializados
│ └── classifier/sentiment-v2.pkl
└── responses/ ← Logs de inferencia
└── 2026/03/08/
├── resp-001.json
└── resp-002.json
Conceptos clave:
- Bucket: Contenedor de nivel superior. Nombre globalmente único en AWS. Uno por proyecto o por entorno (dev, staging, prod).
- Objeto: Un archivo almacenado en S3. Cada objeto tiene una key (ruta), contenido (bytes), y metadata.
- Prefijo: S3 no tiene carpetas reales.
prompts/summarizer/v1/system.txtes una key plana. Los "/" son convención para organizar. - Versioning: S3 puede mantener múltiples versiones del mismo objeto. Útil para prompt templates: rollback a versión anterior si la nueva empeora resultados.
¿Por qué S3 para AI y no una base de datos?
| Criterio | S3 | Base de datos (PostgreSQL, etc.) |
|---|---|---|
| Archivos grandes (modelos, embeddings) | ✅ Sin límite práctico (5TB/objeto) | ❌ No diseñado para BLOBs grandes |
| Coste por GB/mes | $0.023/GB (S3 Standard) | $0.10-0.30/GB (RDS) |
| Acceso por HTTP (presigned URLs) | ✅ Nativo | ❌ Requiere proxy |
| Triggers a Lambda | ✅ S3 Event Notifications | ❌ Requiere polling o CDC |
| Queries complejas | ❌ Solo key-based | ✅ SQL completo |
| Transacciones ACID | ❌ Eventually consistent | ✅ Nativo |
Regla práctica: Archivos + acceso por key + triggers → S3. Datos estructurados + queries complejas → Base de datos. Para AI, la mayoría de assets son archivos (modelos, embeddings, documentos), así que S3 es la opción natural.
Organización de Bucket para Proyectos AI
Patrón recomendado
BUCKET_STRUCTURE = {
"prompts/": "Prompt templates versionados. Tu Lambda los lee antes de invocar al LLM.",
"documents/": "Documentos fuente para RAG. PDFs, JSONs, markdown.",
"embeddings/": "Vectores pre-computados. Se generan offline, se consumen en inferencia.",
"models/": "Modelos serializados (pickle, joblib, ONNX). Para inferencia local o SageMaker.",
"responses/": "Logs de inferencia. Cada respuesta del LLM se guarda para auditoría.",
"config/": "Configuración del sistema. Feature flags, parámetros de modelo.",
}
Naming conventions
# ✅ Buenos nombres de key
"prompts/summarizer/v2/system.txt" # Versionado explícito
"documents/product-docs/manual-2026-q1.pdf" # Temporal
"responses/2026/03/08/req-a1b2c3.json" # Particionado por fecha
"embeddings/product-docs/chunk-embeddings.npy"
# ❌ Malos nombres de key
"prompt.txt" # Sin contexto
"data/file1.json" # No descriptivo
"embeddings.npy" # Sin organización
"responses/latest.json" # Se sobrescribe
Bucket por entorno
# Opción 1: Un bucket por entorno (recomendado para equipos)
BUCKETS = {
"dev": "ai-assets-dev-123456789012",
"staging": "ai-assets-staging-123456789012",
"prod": "ai-assets-prod-123456789012",
}
# Opción 2: Un bucket con prefijo por entorno (más simple)
BUCKET = "ai-assets-123456789012"
PREFIXES = {
"dev": "dev/",
"staging": "staging/",
"prod": "prod/",
}
Operaciones S3 con boto3
Setup del cliente
import boto3
import json
import os
from datetime import datetime
def get_s3_client():
"""Crea un cliente S3 compatible con LocalStack y AWS."""
endpoint_url = os.environ.get("AWS_ENDPOINT_URL")
if endpoint_url:
return boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", "test"),
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", "test"),
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
)
return boto3.client("s3")
s3 = get_s3_client()
Crear bucket
def create_ai_bucket(bucket_name: str, region: str = "us-east-1") -> dict:
"""Crea un bucket S3 para assets de AI."""
create_params = {"Bucket": bucket_name}
if region != "us-east-1":
create_params["CreateBucketConfiguration"] = {
"LocationConstraint": region
}
response = s3.create_bucket(**create_params)
print(f"Bucket creado: {bucket_name}")
return response
bucket_name = f"ai-assets-{os.environ.get('AWS_ACCOUNT_ID', 'dev')}"
create_ai_bucket(bucket_name)
put_object — Subir assets de AI
def upload_prompt_template(
bucket: str, name: str, version: str, content: str
) -> dict:
"""Sube un prompt template versionado a S3."""
key = f"prompts/{name}/{version}/system.txt"
response = s3.put_object(
Bucket=bucket,
Key=key,
Body=content.encode("utf-8"),
ContentType="text/plain",
Metadata={
"template-name": name,
"version": version,
"created-at": datetime.utcnow().isoformat(),
},
)
print(f"Prompt template subido: s3://{bucket}/{key}")
return response
upload_prompt_template(
bucket=bucket_name,
name="summarizer",
version="v1",
content=(
"Eres un asistente especializado en generar resúmenes.\n"
"Genera un resumen conciso del siguiente documento.\n"
"Máximo 3 párrafos. Usa lenguaje claro y directo.\n"
"Incluye los puntos más importantes."
),
)
def upload_rag_document(bucket: str, collection: str, doc: dict) -> dict:
"""Sube un documento para RAG a S3."""
doc_id = doc.get("id", datetime.utcnow().strftime("%Y%m%d%H%M%S"))
key = f"documents/{collection}/{doc_id}.json"
response = s3.put_object(
Bucket=bucket,
Key=key,
Body=json.dumps(doc, ensure_ascii=False).encode("utf-8"),
ContentType="application/json",
Metadata={
"collection": collection,
"doc-id": doc_id,
},
)
print(f"Documento RAG subido: s3://{bucket}/{key}")
return response
upload_rag_document(
bucket=bucket_name,
collection="product-docs",
doc={
"id": "doc-001",
"title": "Guía de instalación",
"content": "Para instalar el sistema, sigue estos pasos...",
"metadata": {"category": "setup", "language": "es"},
},
)
def upload_inference_response(
bucket: str, request_id: str, response_data: dict
) -> dict:
"""Guarda la respuesta de una inferencia para auditoría."""
now = datetime.utcnow()
key = f"responses/{now.strftime('%Y/%m/%d')}/{request_id}.json"
payload = {
"request_id": request_id,
"timestamp": now.isoformat(),
**response_data,
}
result = s3.put_object(
Bucket=bucket,
Key=key,
Body=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
ContentType="application/json",
)
print(f"Response guardada: s3://{bucket}/{key}")
return result
get_object — Recuperar assets
def get_prompt_template(bucket: str, name: str, version: str) -> str:
"""Lee un prompt template de S3."""
key = f"prompts/{name}/{version}/system.txt"
response = s3.get_object(Bucket=bucket, Key=key)
content = response["Body"].read().decode("utf-8")
return content
template = get_prompt_template(bucket_name, "summarizer", "v1")
print(template)
def get_rag_document(bucket: str, collection: str, doc_id: str) -> dict:
"""Lee un documento RAG de S3."""
key = f"documents/{collection}/{doc_id}.json"
response = s3.get_object(Bucket=bucket, Key=key)
content = response["Body"].read().decode("utf-8")
return json.loads(content)
doc = get_rag_document(bucket_name, "product-docs", "doc-001")
print(doc["title"])
list_objects_v2 — Listar assets
def list_ai_assets(bucket: str, prefix: str, max_keys: int = 100) -> list:
"""Lista assets de AI por prefijo."""
response = s3.list_objects_v2(
Bucket=bucket,
Prefix=prefix,
MaxKeys=max_keys,
)
objects = []
for obj in response.get("Contents", []):
objects.append({
"key": obj["Key"],
"size_bytes": obj["Size"],
"last_modified": obj["LastModified"].isoformat(),
})
return objects
prompts = list_ai_assets(bucket_name, "prompts/")
for p in prompts:
print(f" {p['key']} ({p['size_bytes']} bytes)")
def list_all_with_pagination(bucket: str, prefix: str) -> list:
"""Lista todos los objetos, manejando paginación para >1000 resultados."""
paginator = s3.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=bucket, Prefix=prefix)
all_objects = []
for page in pages:
for obj in page.get("Contents", []):
all_objects.append(obj["Key"])
return all_objects
generate_presigned_url — URLs temporales
def generate_download_url(
bucket: str, key: str, expires_in: int = 3600
) -> str:
"""Genera una URL temporal para descargar un objeto sin credenciales AWS."""
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": key},
ExpiresIn=expires_in,
)
return url
url = generate_download_url(bucket_name, "documents/product-docs/doc-001.json")
print(f"URL temporal (1h): {url}")
Presigned URLs son útiles cuando:
- Tu frontend necesita descargar un documento RAG directamente de S3 (sin pasar por Lambda)
- Quieres compartir un modelo serializado con un compañero de equipo
- Una Lambda genera un reporte y envía el link por email/webhook
def generate_upload_url(
bucket: str, key: str, content_type: str = "application/json", expires_in: int = 900
) -> str:
"""Genera una URL temporal para que un cliente suba un archivo directo a S3."""
url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": bucket,
"Key": key,
"ContentType": content_type,
},
ExpiresIn=expires_in,
)
return url
upload_url = generate_upload_url(
bucket_name,
"documents/uploads/user-doc-abc123.pdf",
content_type="application/pdf",
)
print(f"Upload URL (15min): {upload_url}")
delete_object — Limpiar assets obsoletos
def delete_old_responses(bucket: str, prefix: str, dry_run: bool = True) -> list:
"""Elimina responses antiguas de S3."""
objects = list_all_with_pagination(bucket, prefix)
if dry_run:
print(f"[DRY RUN] Se eliminarían {len(objects)} objetos:")
for key in objects[:5]:
print(f" - {key}")
if len(objects) > 5:
print(f" ... y {len(objects) - 5} más")
return objects
if not objects:
return []
delete_keys = [{"Key": key} for key in objects]
response = s3.delete_objects(
Bucket=bucket,
Delete={"Objects": delete_keys},
)
deleted = response.get("Deleted", [])
print(f"Eliminados: {len(deleted)} objetos")
return [d["Key"] for d in deleted]
Módulo Reutilizable: S3 AI Assets Manager
"""s3_ai_assets.py — Gestión de AI assets en S3."""
import boto3
import json
import os
from datetime import datetime
from typing import Optional
class S3AIAssets:
"""Gestiona assets de AI en un bucket S3."""
def __init__(self, bucket_name: str, endpoint_url: Optional[str] = None):
self.bucket = bucket_name
client_kwargs = {}
if endpoint_url:
client_kwargs["endpoint_url"] = endpoint_url
self.s3 = boto3.client("s3", **client_kwargs)
def put_prompt(self, name: str, version: str, content: str) -> str:
key = f"prompts/{name}/{version}/system.txt"
self.s3.put_object(
Bucket=self.bucket,
Key=key,
Body=content.encode("utf-8"),
ContentType="text/plain",
Metadata={"version": version, "updated": datetime.utcnow().isoformat()},
)
return key
def get_prompt(self, name: str, version: str) -> str:
key = f"prompts/{name}/{version}/system.txt"
resp = self.s3.get_object(Bucket=self.bucket, Key=key)
return resp["Body"].read().decode("utf-8")
def put_document(self, collection: str, doc_id: str, data: dict) -> str:
key = f"documents/{collection}/{doc_id}.json"
self.s3.put_object(
Bucket=self.bucket,
Key=key,
Body=json.dumps(data, ensure_ascii=False).encode("utf-8"),
ContentType="application/json",
)
return key
def get_document(self, collection: str, doc_id: str) -> dict:
key = f"documents/{collection}/{doc_id}.json"
resp = self.s3.get_object(Bucket=self.bucket, Key=key)
return json.loads(resp["Body"].read().decode("utf-8"))
def put_response(self, request_id: str, data: dict) -> str:
now = datetime.utcnow()
key = f"responses/{now.strftime('%Y/%m/%d')}/{request_id}.json"
payload = {"request_id": request_id, "timestamp": now.isoformat(), **data}
self.s3.put_object(
Bucket=self.bucket,
Key=key,
Body=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
ContentType="application/json",
)
return key
def list_keys(self, prefix: str) -> list:
paginator = self.s3.get_paginator("list_objects_v2")
keys = []
for page in paginator.paginate(Bucket=self.bucket, Prefix=prefix):
for obj in page.get("Contents", []):
keys.append(obj["Key"])
return keys
def presigned_download(self, key: str, expires: int = 3600) -> str:
return self.s3.generate_presigned_url(
"get_object",
Params={"Bucket": self.bucket, "Key": key},
ExpiresIn=expires,
)
# Uso
assets = S3AIAssets(
bucket_name="ai-assets-dev",
endpoint_url=os.environ.get("AWS_ENDPOINT_URL"),
)
assets.put_prompt("summarizer", "v1", "Resume el siguiente documento en 3 puntos.")
template = assets.get_prompt("summarizer", "v1")
print(template)
S3 Lifecycle: Gestión Automática de Costes
Para un sistema AI en producción, los responses se acumulan. Lifecycle rules automatizan la transición a storage más barato o la eliminación:
def configure_ai_lifecycle(bucket: str) -> dict:
"""Configura lifecycle rules para assets de AI."""
response = s3.put_bucket_lifecycle_configuration(
Bucket=bucket,
LifecycleConfiguration={
"Rules": [
{
"ID": "archive-old-responses",
"Filter": {"Prefix": "responses/"},
"Status": "Enabled",
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA",
},
{
"Days": 90,
"StorageClass": "GLACIER",
},
],
"Expiration": {"Days": 365},
},
{
"ID": "clean-temp-uploads",
"Filter": {"Prefix": "temp/"},
"Status": "Enabled",
"Expiration": {"Days": 7},
},
],
},
)
print(f"Lifecycle configurado para {bucket}")
return response
Resultado del lifecycle:
responses/*
├── 0-30 días: S3 Standard ($0.023/GB)
├── 30-90 días: S3 Standard-IA ($0.0125/GB) ← 46% más barato
├── 90-365 días: S3 Glacier ($0.004/GB) ← 83% más barato
└── >365 días: Eliminado automáticamente
temp/*
└── >7 días: Eliminado automáticamente
Troubleshooting
Problema 1: "NoSuchBucket" al hacer put_object
El bucket no existe. Necesitas crearlo antes de subir objetos.
try:
s3.head_bucket(Bucket=bucket_name)
except s3.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "404":
print(f"Bucket {bucket_name} no existe. Creándolo...")
s3.create_bucket(Bucket=bucket_name)
else:
raise
Problema 2: "AccessDenied" al leer de S3
Tu rol IAM no tiene permisos para la operación. En LocalStack no pasa; en AWS real, sí.
# Verificar permisos del rol
aws iam get-role-policy --role-name tu-lambda-role --policy-name s3-access
# El policy necesita s3:GetObject en el bucket específico
Problema 3: get_object retorna bytes, no string
response["Body"].read() retorna bytes. Necesitas decodificar.
# ❌ Error común
content = s3.get_object(Bucket=b, Key=k)["Body"].read()
data = json.loads(content) # Funciona porque json.loads acepta bytes
# ✅ Explícito y seguro
content = s3.get_object(Bucket=b, Key=k)["Body"].read().decode("utf-8")
data = json.loads(content)
Problema 4: Presigned URLs no funcionan en LocalStack
LocalStack genera URLs con localhost, que no son accesibles desde fuera de tu máquina.
# En LocalStack, la URL generada es:
# http://localhost:4566/bucket/key?...
# Si accedes desde un container Docker, usa el nombre del servicio:
# http://localstack:4566/bucket/key?...
# Para testing local, localhost funciona.
Problema 5: list_objects_v2 retorna solo 1000 objetos
S3 pagina a 1000 objetos por request. Usa el paginator de boto3.
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=bucket, Prefix="responses/"):
for obj in page.get("Contents", []):
print(obj["Key"])
Ejercicios Prácticos
Ejercicio 1: Versionado de prompt templates
Crea una función que suba múltiples versiones de un prompt template a S3 y otra que recupere la última versión disponible (la de número más alto).
Ver solución
import boto3
import json
import os
import re
s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-dev"
def upload_prompt_versions(name: str, versions: dict[str, str]) -> list:
"""Sube múltiples versiones de un prompt. versions = {"v1": "...", "v2": "..."}"""
keys = []
for version, content in versions.items():
key = f"prompts/{name}/{version}/system.txt"
s3.put_object(
Bucket=BUCKET,
Key=key,
Body=content.encode("utf-8"),
ContentType="text/plain",
)
keys.append(key)
print(f"Subido: {key}")
return keys
def get_latest_prompt(name: str) -> tuple[str, str]:
"""Retorna (version, content) de la última versión del prompt."""
response = s3.list_objects_v2(
Bucket=BUCKET,
Prefix=f"prompts/{name}/",
)
versions = []
for obj in response.get("Contents", []):
match = re.search(r"/v(\d+)/", obj["Key"])
if match:
versions.append((int(match.group(1)), obj["Key"]))
if not versions:
raise ValueError(f"No versions found for prompt '{name}'")
versions.sort(key=lambda x: x[0], reverse=True)
latest_key = versions[0][1]
latest_version = f"v{versions[0][0]}"
resp = s3.get_object(Bucket=BUCKET, Key=latest_key)
content = resp["Body"].read().decode("utf-8")
return latest_version, content
upload_prompt_versions("summarizer", {
"v1": "Resume el documento en un párrafo.",
"v2": "Resume el documento en 3 bullets. Sé conciso.",
"v3": "Resume el documento en 3 bullets. Incluye datos numéricos si existen.",
})
version, content = get_latest_prompt("summarizer")
print(f"Última versión: {version}")
print(f"Contenido: {content}")
Ejercicio 2: Bulk upload de documentos RAG
Implementa una función que tome una lista de documentos (dicts) y los suba a S3 en una colección específica. Incluye un contador de éxitos y fallos.
Ver solución
import boto3
import json
import os
from dataclasses import dataclass
s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-dev"
@dataclass
class UploadResult:
total: int
success: int
failed: int
errors: list
def bulk_upload_documents(collection: str, documents: list[dict]) -> UploadResult:
"""Sube una lista de documentos a S3 para RAG."""
result = UploadResult(total=len(documents), success=0, failed=0, errors=[])
for doc in documents:
doc_id = doc.get("id")
if not doc_id:
result.failed += 1
result.errors.append({"doc": doc, "error": "Missing 'id' field"})
continue
key = f"documents/{collection}/{doc_id}.json"
try:
s3.put_object(
Bucket=BUCKET,
Key=key,
Body=json.dumps(doc, ensure_ascii=False).encode("utf-8"),
ContentType="application/json",
)
result.success += 1
except Exception as e:
result.failed += 1
result.errors.append({"doc_id": doc_id, "error": str(e)})
print(f"Upload completado: {result.success}/{result.total} exitosos")
if result.errors:
print(f" Errores: {result.failed}")
return result
docs = [
{"id": "faq-001", "question": "¿Cómo instalo el SDK?", "answer": "Usa pip install..."},
{"id": "faq-002", "question": "¿Cuál es el pricing?", "answer": "Plan free + plan pro..."},
{"id": "faq-003", "question": "¿Hay API pública?", "answer": "Sí, documentada en..."},
]
result = bulk_upload_documents("faq", docs)
print(f"Resultado: {result}")
Ejercicio 3: Exportar colección RAG completa
Implementa una función que descargue todos los documentos de una colección RAG de S3 y los retorne como una lista de dicts. Debe manejar paginación.
Ver solución
import boto3
import json
import os
s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-dev"
def export_collection(collection: str) -> list[dict]:
"""Descarga todos los documentos de una colección RAG."""
prefix = f"documents/{collection}/"
paginator = s3.get_paginator("list_objects_v2")
documents = []
for page in paginator.paginate(Bucket=BUCKET, Prefix=prefix):
for obj in page.get("Contents", []):
key = obj["Key"]
if not key.endswith(".json"):
continue
try:
response = s3.get_object(Bucket=BUCKET, Key=key)
content = response["Body"].read().decode("utf-8")
doc = json.loads(content)
doc["_s3_key"] = key
doc["_s3_size"] = obj["Size"]
doc["_s3_modified"] = obj["LastModified"].isoformat()
documents.append(doc)
except Exception as e:
print(f"Error leyendo {key}: {e}")
print(f"Exportados {len(documents)} documentos de '{collection}'")
return documents
docs = export_collection("faq")
for doc in docs:
print(f" {doc['id']}: {doc.get('question', 'N/A')}")
Ejercicio 4: Auditoría de uso de storage
Crea una función que analice el contenido de un bucket y genere un reporte de uso por prefijo (tamaño total, número de objetos, objeto más reciente).
Ver solución
import boto3
import os
from collections import defaultdict
from datetime import datetime
s3 = boto3.client("s3", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-dev"
def storage_audit(bucket: str) -> dict:
"""Genera un reporte de uso de storage por prefijo de primer nivel."""
paginator = s3.get_paginator("list_objects_v2")
stats = defaultdict(lambda: {
"count": 0,
"total_bytes": 0,
"latest_modified": None,
"latest_key": None,
})
for page in paginator.paginate(Bucket=bucket):
for obj in page.get("Contents", []):
key = obj["Key"]
prefix = key.split("/")[0] + "/" if "/" in key else "(root)"
stats[prefix]["count"] += 1
stats[prefix]["total_bytes"] += obj["Size"]
mod_time = obj["LastModified"]
if (
stats[prefix]["latest_modified"] is None
or mod_time > stats[prefix]["latest_modified"]
):
stats[prefix]["latest_modified"] = mod_time
stats[prefix]["latest_key"] = key
report = dict(stats)
total_bytes = sum(s["total_bytes"] for s in report.values())
total_objects = sum(s["count"] for s in report.values())
print(f"=== Storage Audit: {bucket} ===")
print(f"Total: {total_objects} objetos, {total_bytes / 1024:.1f} KB")
print()
for prefix, data in sorted(report.items()):
size_kb = data["total_bytes"] / 1024
print(f" {prefix:<20} {data['count']:>5} objetos {size_kb:>8.1f} KB")
if data["latest_key"]:
print(f" {'':20} último: {data['latest_key']}")
print()
estimated_monthly = (total_bytes / (1024**3)) * 0.023
print(f"Coste S3 estimado: ${estimated_monthly:.4f}/mes (S3 Standard)")
return report
storage_audit(BUCKET)
Resumen
- S3 es el data layer de tu sistema AI. No es almacenamiento genérico — es donde viven tus prompt templates, documentos RAG, embeddings, modelos, y logs de inferencia.
- Organiza por prefijo con convención clara:
prompts/,documents/,embeddings/,models/,responses/. Versiona explícitamente. - boto3 operations clave:
put_objectpara subir,get_objectpara leer,list_objects_v2con paginator para listar,generate_presigned_urlpara URLs temporales. - Lifecycle rules automatizan costes: Mueve responses antiguos a storage barato (IA, Glacier) y elimina archivos temporales automáticamente.
- El módulo S3AIAssets es reutilizable. Lo usarás en la cápsula 04 (integración S3+Lambda) y en el proyecto del módulo.
- Todo funciona igual en LocalStack y AWS. La diferencia es
endpoint_urlen boto3.
Recursos Adicionales
- S3 User Guide — Documentación oficial completa
- boto3 S3 Examples — Ejemplos oficiales de boto3 para S3
- S3 Presigned URLs — Guía de URLs temporales
- S3 Lifecycle Configuration — Gestión de lifecycle
- S3 Storage Classes — Comparativa de clases de almacenamiento
- S3 Pricing — Pricing detallado de S3
- S3 Event Notifications — Triggers S3 → Lambda
- S3 Best Practices — Optimización de rendimiento