# AWS — Referencia Completa de Servicios

## S3 — Almacenamiento Seguro

```python
import boto3
from botocore.exceptions import ClientError

cliente_s3 = boto3.client('s3')

# OBLIGATORIO: cifrado en reposo + versionado + bloquear acceso público
def configurar_bucket_seguro(nombre_bucket: str) -> None:
    s3 = boto3.client('s3')

    # Bloquear acceso público (SIEMPRE para datos sensibles)
    s3.put_public_access_block(
        Bucket=nombre_bucket,
        PublicAccessBlockConfiguration={
            'BlockPublicAcls': True,
            'IgnorePublicAcls': True,
            'BlockPublicPolicy': True,
            'RestrictPublicBuckets': True,
        }
    )

    # Cifrado AES-256 o KMS
    s3.put_bucket_encryption(
        Bucket=nombre_bucket,
        ServerSideEncryptionConfiguration={
            'Rules': [{'ApplyServerSideEncryptionByDefault': {
                'SSEAlgorithm': 'aws:kms',
                'KMSMasterKeyID': 'arn:aws:kms:...',
            }}]
        }
    )

    # Versionado (recuperación ante errores)
    s3.put_bucket_versioning(
        Bucket=nombre_bucket,
        VersioningConfiguration={'Status': 'Enabled'}
    )

# URLs prefirmadas para descarga sin exponer bucket público
def generar_url_descarga(bucket: str, clave: str, segundos: int = 3600) -> str:
    return cliente_s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket, 'Key': clave},
        ExpiresIn=segundos,
    )

# Ciclo de vida — bajar costos moviendo a clases más baratas
lifecycle_policy = {
    'Rules': [{
        'ID': 'ArchivarDocumentosViejos',
        'Status': 'Enabled',
        'Transitions': [
            {'Days': 30, 'StorageClass': 'STANDARD_IA'},
            {'Days': 90, 'StorageClass': 'GLACIER'},
            {'Days': 365, 'StorageClass': 'DEEP_ARCHIVE'},
        ],
        'Expiration': {'Days': 2555},  # 7 años
    }]
}
```

---

## RDS — Base de Datos en Producción

```python
# Configuración recomendada para PostgreSQL en RDS
rds_config = {
    "DBInstanceClass": "db.t3.medium",
    "Engine": "postgres",
    "EngineVersion": "15.4",
    "MultiAZ": True,                        # OBLIGATORIO en producción
    "StorageEncrypted": True,               # OBLIGATORIO
    "BackupRetentionPeriod": 7,
    "EnablePerformanceInsights": True,
    "PerformanceInsightsRetentionPeriod": 7,
    "MonitoringInterval": 60,
    "DeletionProtection": True,
    "StorageType": "gp3",
    "Iops": 3000,
}

# Connection pool recomendado para Python
DATABASE_URL = (
    f"postgresql+asyncpg://{usuario}:{password}@{host}:{puerto}/{db}"
    "?ssl=require"
)

# Pool sizing: (núcleos_cpu * 2) + disco_efectivo
engine = create_async_engine(
    DATABASE_URL,
    pool_size=5,
    max_overflow=10,
    pool_pre_ping=True,
    pool_recycle=3600,
)
```

---

## Lambda — Funciones Serverless

```python
import json
import logging
import os
from typing import Any

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Inicializar clientes FUERA del handler (reutilización en warm start)
import boto3
sqs = boto3.client('sqs')
s3 = boto3.client('s3')

def handler(event: dict, context: Any) -> dict:
    """Lambda handler para procesamiento de eventos SQS."""
    logger.info("Evento recibido: %s registros", len(event.get('Records', [])))

    errores = []
    for record in event.get('Records', []):
        try:
            body = json.loads(record['body'])
            procesar_registro(body)
        except Exception as e:
            logger.error("Error en record %s: %s", record['messageId'], e)
            errores.append({'itemIdentifier': record['messageId']})

    # Partial batch response — solo reintentar los que fallaron
    return {'batchItemFailures': errores}

# Configuración recomendada
lambda_config = {
    "timeout": 30,
    "memory_size": 512,
    "reserved_concurrency": 10,
    "environment": {
        "QUEUE_URL": "${ssm:/prod/cola-procesamiento}",
        "LOG_LEVEL": "INFO",
    },
    "layers": ["arn:aws:lambda:us-east-1:...:layer:deps:5"],
}
```

---

## ECS Fargate — Containers Administrados

```yaml
# task-definition.json
{
  "family": "api-nomina",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::...:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::...:role/ecsTaskRole",
  "containerDefinitions": [{
    "name": "api",
    "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/api-nomina:latest",
    "portMappings": [{"containerPort": 8000, "protocol": "tcp"}],
    "environment": [],
    "secrets": [
      {
        "name": "DATABASE_URL",
        "valueFrom": "arn:aws:secretsmanager:...:secret:prod/db-url"
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/api-nomina",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "healthCheck": {
      "command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"],
      "interval": 30,
      "timeout": 5,
      "retries": 3,
      "startPeriod": 60
    }
  }]
}
```

---

## SQS/SNS — Mensajería Desacoplada

```python
import boto3
import json
from typing import Any

sqs = boto3.client('sqs', region_name='us-east-1')
sns = boto3.client('sns', region_name='us-east-1')

QUEUE_URL = os.environ['QUEUE_URL']

def enviar_mensaje(payload: dict, grupo_id: str | None = None) -> str:
    """Enviar mensaje a SQS. grupo_id para FIFO queues."""
    kwargs: dict[str, Any] = {
        'QueueUrl': QUEUE_URL,
        'MessageBody': json.dumps(payload, ensure_ascii=False),
        'MessageAttributes': {
            'ContentType': {
                'StringValue': 'application/json',
                'DataType': 'String',
            }
        },
    }
    if grupo_id:
        import uuid
        kwargs['MessageGroupId'] = grupo_id
        kwargs['MessageDeduplicationId'] = str(uuid.uuid4())

    respuesta = sqs.send_message(**kwargs)
    return respuesta['MessageId']

def procesar_mensajes(max_mensajes: int = 10) -> None:
    """Polling de mensajes con visibilidad extendida."""
    mensajes = sqs.receive_message(
        QueueUrl=QUEUE_URL,
        MaxNumberOfMessages=max_mensajes,
        WaitTimeSeconds=20,
        VisibilityTimeout=60,
        AttributeNames=['All'],
    ).get('Messages', [])

    for msg in mensajes:
        try:
            body = json.loads(msg['Body'])
            procesar(body)
            sqs.delete_message(
                QueueUrl=QUEUE_URL,
                ReceiptHandle=msg['ReceiptHandle'],
            )
        except Exception as e:
            logger.error("Error procesando mensaje %s: %s", msg['MessageId'], e)

# Dead Letter Queue — SIEMPRE configurar para auditoría
dlq_config = {
    'deadLetterTargetArn': 'arn:aws:sqs:...:cola-nomina-dlq',
    'maxReceiveCount': '3',
}
```

---

## CloudFront — CDN y Caché

```python
# Para frontend SPA en S3 + CloudFront
distribution_config = {
    "Origins": [{
        "DomainName": "mi-bucket.s3.amazonaws.com",
        "S3OriginConfig": {"OriginAccessIdentity": "origin-access-identity/cloudfront/..."},
    }],
    "DefaultCacheBehavior": {
        "ViewerProtocolPolicy": "redirect-to-https",
        "CachePolicyId": "...",
        "ResponseHeadersPolicyId": "...",
    },
    "CustomErrorResponses": [
        {"ErrorCode": 404, "ResponseCode": 200, "ResponsePagePath": "/index.html"},
        {"ErrorCode": 403, "ResponseCode": 200, "ResponsePagePath": "/index.html"},
    ],
    "HttpVersion": "http2and3",
    "PriceClass": "PriceClass_100",
}
```

---

## Optimización de Costos

### Estrategias principales

| Servicio | Optimización | Ahorro estimado |
|----------|-------------|-----------------|
| EC2/ECS | Spot instances para workers | 60-90% |
| EC2 | Reserved instances (1-3 años) | 30-60% |
| RDS | Stop en entornos de dev/staging | 60-70% |
| S3 | Lifecycle policies | 40-80% en archivos viejos |
| Lambda | Ajustar memoria (benchmark) | 20-50% |
| CloudFront | Comprimir respuestas | Reduce transferencia |
| Data Transfer | Mantener tráfico en misma AZ/región | Evita cargos |

```python
# AWS Cost Anomaly Detection — alertas automáticas
import boto3

ce = boto3.client('ce', region_name='us-east-1')

def crear_monitor_costos(email: str) -> None:
    monitor = ce.create_anomaly_monitor(
        AnomalyMonitor={
            'MonitorName': 'MonitorGeneral',
            'MonitorType': 'DIMENSIONAL',
            'MonitorDimension': 'SERVICE',
        }
    )

    ce.create_anomaly_subscription(
        AnomalySubscription={
            'MonitorArnList': [monitor['MonitorArn']],
            'Subscribers': [{'Address': email, 'Type': 'EMAIL'}],
            'Threshold': 20.0,
            'Frequency': 'DAILY',
            'SubscriptionName': 'AlertasCostoDiario',
        }
    )
```
