Casos y Problemas en AWS — Entrevistas Senior
This content is not available in your language yet.
Casos de análisis de problemas reales: diagnóstico paso a paso, solución y prevención. Todos son de tipo [CASO].
1. Lambda con cold starts inaceptables en una API de pagos
Sección titulada «1. Lambda con cold starts inaceptables en una API de pagos»Categoría: Cómputo / Performance · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»API en Java/Spring sobre Lambda con p99 de 8 s en horas valle. Diagnóstico: métricas de InitDuration en CloudWatch/X-Ray confirman cold starts de 6-9 s concentrados tras períodos de inactividad y en picos de escalado. Solución en capas: optimizar el init (dependencias, SnapStart o AOT), provisioned concurrency dimensionada al piso de concurrencia real, y si el patrón de tráfico es sostenido, replantear el compute (Fargate). Prevención: presupuesto de init en CI y alarmas sobre p99 por percentil, no promedio.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: checkout con Lambda Java 17 + Spring Boot detrás de API Gateway. Quejas de timeouts esporádicos; el promedio de latencia parece sano (180 ms), pero el p99 en horas valle sube a 8 s y hay 504 del API Gateway (timeout 29 s no, pero el cliente corta a 5 s).
Diagnóstico paso a paso: (1) CloudWatch Logs Insights sobre los REPORT de la función: filter @type="REPORT" | stats avg(@initDuration), pct(@initDuration, 99), count() by bin(5m) — aparece initDuration de 6-9 s en un 2% de invocaciones, correlado con valles de tráfico y con ráfagas (cada escalado crea entornos nuevos). (2) X-Ray: los traces lentos muestran el segmento Initialization dominando, y dentro del init, el arranque del contexto Spring y la carga de 40 MB de dependencias. (3) Confirmar el patrón: ConcurrentExecutions oscila entre 2 y 60 — cada subida crea decenas de cold starts simultáneos. (4) Descartar factores agravantes: ¿VPC? (ya no penaliza significativamente), ¿tamaño del artefacto? (fat jar de 85 MB), ¿memoria? (512 MB = poca CPU para el init: el init también usa la CPU proporcional… aunque Lambda da boost de CPU durante el init, el JIT sufre después).
Solución por capas, de menor a mayor esfuerzo: (1) Recortar el init: eliminar dependencias no usadas, lazy init de Spring (spring.main.lazy-initialization=true), inicializar clientes AWS fuera del handler pero solo los necesarios, subir memoria a 1024-2048 MB (mejora init y ejecución; a menudo el costo total baja porque la duración cae). (2) SnapStart (gratis en Java): publica versión, snapshot del entorno inicializado, restauración en ~200-500 ms — recorte del 90% del cold start; revisar hooks beforeCheckpoint/afterRestore para conexiones y semillas aleatorias. Alternativa: recompilar con Quarkus/Spring Native (GraalVM) para inits de <500 ms. (3) Provisioned concurrency sobre el alias: dimensionar al piso de concurrencia del horario de negocio (p. ej. 20), con Application Auto Scaling por horario; costo ~$11/mes por entorno de 1 GB — 20 entornos ≈ $220/mes, a comparar contra el costo de negocio de los timeouts. (4) Replantear el compute: si la concurrencia sostenida es alta y estable, este workload es más barato y predecible en Fargate tras un ALB; los picos irregulares justifican Lambda, el tráfico plano no.
Prevención: test de arranque en CI que falla si el init supera un presupuesto (p. ej. 2 s), alarmas sobre p99/p99.9 (jamás promedios), dashboard con InitDuration y ratio de cold starts, y revisión del patrón de tráfico real antes de elegir compute en el diseño. En la entrevista remato con el meta-punto: el cold start no era el bug; el bug fue elegir Java/Spring pesado sobre Lambda para tráfico con valles sin presupuestar el init.
2. DynamoDB con throttling por hot partition en un multi-tenant
Sección titulada «2. DynamoDB con throttling por hot partition en un multi-tenant»Categoría: Bases de datos · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Errores ProvisionedThroughputExceededException con la tabla al 20% de su capacidad agregada: síntoma inequívoco de hot partition. Diagnóstico: CloudWatch Contributor Insights identifica la partition key dominante (un tenant enorme); las métricas por GSI revelan si el cuello está en un índice. Solución: write sharding para esa clave, caché de lecturas (DAX), y aislar al tenant gigante. Prevención: diseño de claves validado contra el límite físico de 1,000 WCU/3,000 RCU por partición y monitoreo de cardinalidad desde el día uno.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: SaaS multi-tenant con tabla DynamoDB PK=TENANT#id, SK=EVENT#timestamp en modo on-demand. Un lunes, el tenant más grande lanza una campaña y las escrituras de ese tenant empiezan a fallar con throttling, aunque la métrica ConsumedWriteCapacityUnits de la tabla está lejos de cualquier límite. Los retries del SDK amplifican la carga y la latencia p99 se dispara para ese tenant.
Diagnóstico paso a paso: (1) CloudWatch: WriteThrottleEvents > 0 con consumo agregado bajo → no es un problema de capacidad de tabla sino de partición: el límite físico es 1,000 WCU/s y 3,000 RCU/s por partición, y on-demand no lo exime. (2) Contributor Insights para DynamoDB (activarlo en la tabla): el reporte “most throttled keys” muestra que TENANT#biggest concentra el 85% de los throttles. (3) Revisar GSIs: OnlineIndexThrottleEvents y las métricas del GSI — un GSI con GSI1PK=TENANT#id replica el problema y, peor, el throttling de un GSI contrapresiona las escrituras de la tabla base. (4) Revisar el patrón de escritura: eventos de campaña con timestamps consecutivos, todos a la misma PK — escritura secuencial pura sobre una partición.
Solución: (1) Write sharding: cambiar la clave a TENANT#id#SHARD-n con n = hash(eventId) % 10 — reparte el tenant caliente entre 10 particiones (10,000 WCU/s teóricas); las lecturas por rango hacen 10 queries en paralelo y fusionan (o un GSI agregador si el patrón de lectura lo permite). El número de shards se calcula: pico esperado de WCU / 1,000, con margen. (2) Mitigación inmediata sin migración: poner SQS delante de las escrituras de eventos (buffer que aplana el pico y controla el ritmo de consumo) — convierte throttling en latencia asumible para un caso de ingesta. (3) Lecturas calientes: DAX (caché write-through de items y queries con TTL, latencia de microsegundos) o ElastiCache para dashboards del tenant. (4) Estratégico: aislar tenants gigantes — tabla dedicada o partición de servicio para el tier enterprise; el modelo “todos los tenants en la misma clave” no sobrevive a la asimetría de tamaños real (regla 80/20).
Prevención: en la revisión de diseño, para cada patrón de acceso preguntar “¿cuál es el pico de tráfico de la clave más caliente?” y compararlo con 1,000/3,000; Contributor Insights activado en tablas críticas con alarma sobre throttles; pruebas de carga con distribución realista de tenants (no uniforme); y en on-demand, recordar que el warm throughput inicial (~4,000 WCU de tabla) también existe — para eventos tipo Black Friday, pre-calentar con warm throughput o switch temporal a provisioned alto. Distingo siempre en la entrevista: subir capacidad no arregla una hot partition; solo el diseño de claves lo hace.
3. La factura de AWS se triplicó este mes: análisis
Sección titulada «3. La factura de AWS se triplicó este mes: análisis»Categoría: FinOps · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Metodología: Cost Explorer agrupado por servicio y luego por usage type para aislar el rubro que creció; Cost Anomaly Detection y CloudTrail para fechar el cambio y encontrar el recurso/deploy causante; CUR con Athena para el detalle por recurso. Los culpables típicos: NAT Gateway/data transfer por un cambio de arquitectura, logs de CloudWatch en DEBUG, un bucle de Lambda recursivo, entornos clonados sin apagar, o snapshots/almacenamiento acumulado. Solución dirigida al rubro y prevención con budgets, anomaly detection y tagging.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: factura pasa de $18k a $54k. Dirección pide explicación hoy.
Diagnóstico paso a paso: (1) Cost Explorer, vista diaria del mes, group by Service: identifica el servicio que se disparó (supongamos EC2-Other y CloudWatch). (2) Re-agrupar por Usage Type dentro del servicio: “EC2-Other” casi siempre es NatGateway-Bytes ($0.045/GB), DataTransfer-Regional-Bytes (cross-AZ) o volúmenes EBS; CloudWatch suele ser DataProcessing-Bytes (ingesta de logs $0.50/GB). Con el usage type exacto, la fecha de inflexión es visible al día. (3) Fechar y correlacionar: ¿qué se desplegó ese día? CloudTrail (eventos de infra: creación de NAT, cambios de route tables, nuevos servicios), historial de deploys, PRs de IaC. (4) Atribuir al recurso: Cost Explorer con group by tag (si hay tagging) o CUR/Data Exports en Athena consultando line_item_resource_id — qué NAT, qué log group, qué bucket concreto. (5) Casos arquetípicos que verifico en orden: logs en DEBUG o un log loop (un log group ingiriendo TB — Logs Insights: stats sum(@billedBytes) by @logGroup en la consola de métricas IncomingBytes por log group); Lambda recursiva (S3 PUT que dispara Lambda que escribe al mismo bucket — la “recursive loop detection” de Lambda ayuda pero no cubre todo); tráfico que antes iba por VPC endpoint y ahora pasa por NAT (alguien borró el endpoint o cambió una ruta); replicación S3 nueva o lifecycle mal hecho generando transiciones masivas ($0.01-0.05/1,000 objetos: transicionar 100M de objetos pequeños cuesta miles); entornos de prueba clonados de prod y olvidados; una campaña de marketing legítima (crecimiento real de tráfico — no todo pico es un bug).
Solución: quirúrgica según el rubro — recrear el VPC endpoint y las rutas, bajar niveles de log y poner retención, romper el bucle recursivo (reserved concurrency 0 como freno de emergencia), borrar recursos zombis, y negociar con el equipo dueño si es crecimiento legítimo (¿el costo unitario se mantuvo?). Documentar el RCA con el timeline exacto.
Prevención: (1) Cost Anomaly Detection con suscripción diaria (habría alertado el día 2, no el día 30); (2) Budgets por cuenta y por servicio con alertas a 80/100% del forecast; (3) tagging obligatorio (team, service, env) forzado por SCP/Config para atribución en minutos; (4) separación por cuentas (el blast radius de costos de un experimento queda contenido y visible); (5) revisión mensual de costos por equipo con costos unitarios (costo por pedido/request), que separa “gastamos más porque vendemos más” de “gastamos más porque algo está roto”; (6) guardarraíles de arquitectura: dashboards de bytes por NAT y por log group, y VPC endpoints como parte del módulo estándar de VPC en IaC.
4. Pods de EKS en CrashLoopBackOff tras un deploy
Sección titulada «4. Pods de EKS en CrashLoopBackOff tras un deploy»Categoría: Contenedores / EKS · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»CrashLoopBackOff significa que el contenedor arranca y muere repetidamente, con backoff creciente. Diagnóstico ordenado: kubectl describe pod (eventos, exit code, OOMKilled), kubectl logs --previous (el log del intento anterior, clave), y de ahí bifurcar: exit code 137/OOM → límites de memoria; 1/error de app → config/secretos/dependencias; probes fallando → liveness mal calibrada matando pods sanos. En EKS se suman causas propias: IRSA mal configurado, agotamiento de IPs del VPC CNI y throttling de dependencias AWS.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: tras desplegar la versión nueva del servicio de catálogo, los pods entran en CrashLoopBackOff; el deployment queda a medias (rolling update detenido porque los pods nuevos nunca están Ready).
Diagnóstico paso a paso: (1) kubectl get pods -o wide — ¿todos los pods nuevos o solo en ciertos nodos/AZ? (patrón por nodo sugiere infraestructura; todos sugiere app/config). (2) kubectl describe pod X: sección Events (¿probe failures?, ¿FailedMount de un secret/configmap?, ¿imagen no baja — ImagePullBackOff es otro diagnóstico—?) y Last State: Terminated con exit code: 137 = SIGKILL, casi siempre OOMKilled (confirmar en Reason); 1/otros = error de aplicación; 143 = SIGTERM (apagado por probe). (3) kubectl logs X --previous — el log del contenedor que murió (el actual estará vacío o a medias): stack trace, “cannot connect to…”, “missing env var…”. (4) Bifurcación por causa: OOMKilled → comparar resources.limits.memory con el consumo real (Container Insights/Prometheus); típico: la versión nueva sube el heap (JVM sin -XX:MaxRAMPercentage que ignora los cgroups y dimensiona el heap según la memoria del nodo). Arranque lento + liveness agresiva → la app tarda 90 s en arrancar y la liveness dispara a los 30: el kubelet mata pods sanos en bucle; usar startupProbe y separar liveness (¿estoy vivo?) de readiness (¿puedo recibir tráfico?). Config/secretos → el deploy introdujo una env var nueva no presente en el ConfigMap del entorno, o el External Secrets no sincronizó. IRSA/permisos → la app llama a AWS y recibe AccessDenied al arrancar y sale: verificar el service account, la annotation del rol, la trust policy OIDC (audiencia sts.amazonaws.com, condición sub con namespace:serviceaccount exactos — el error clásico es el namespace). (5) Si nada de eso: dependencia externa caída en arranque (la app sale si no conecta a la BD — mala práctica: debería reintentar y reportar not-ready).
Solución: rollback primero si hay impacto (kubectl rollout undo deployment/X — y aquí se agradece que el rolling update con maxUnavailable conservador haya mantenido los pods viejos sirviendo), luego el fix según causa: límites de memoria realistas (limits=requests para memoria, evitar overcommit), startupProbe con failureThreshold×periodSeconds > peor arranque real, config validada, IRSA corregido.
Prevención: en CI, helm template + validación de esquema de valores y diff de env vars requeridas contra las provistas; readiness/liveness/startup probes revisadas como parte del PR; recursos derivados de perfiles de carga (VPA en modo recomendación); despliegues canary/progresivos con Argo Rollouts o Flagger que abortan solos ante crash loops; y alarma sobre kube_pod_container_status_restarts_total creciente. El meta-mensaje senior: CrashLoopBackOff no es “un error”, es un síntoma con un árbol de causas — lo que evalúo es el orden del diagnóstico (describe → logs –previous → exit code) antes de tocar nada.
5. Latencia intermitente entre servicios en distintas AZ
Sección titulada «5. Latencia intermitente entre servicios en distintas AZ»Categoría: Networking / Performance · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Latencia p99 con picos intermitentes entre dos microservicios: primero separar red de aplicación con X-Ray (¿dónde se acumula el tiempo?) y métricas por AZ. Causas típicas: cross-AZ inevitable con balanceo no zonal, agotamiento de conexiones/pool con keep-alive mal configurado, DNS resolviendo con latencia, SNAT port exhaustion en NAT, o un nodo/instancia degradado en una AZ. El fix va de habilitar zone-aware routing y pools bien dimensionados a retirar la instancia enferma; la prevención es observabilidad con dimensión de AZ en todas las métricas.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: el servicio A llama al B (ECS tras ALB interno, 3 AZs). El p50 es 8 ms, pero el p99 salta a 300-900 ms en ráfagas de minutos, sin correlación con el tráfico total.
Diagnóstico paso a paso: (1) X-Ray: en los traces lentos, ¿el tiempo está en el segmento remoto (B procesando) o en el hueco cliente-servidor (red/conexión)? Si B reporta 8 ms pero A midió 600 ms, el problema es transporte/conexión/cola de espera. (2) Dimensión de AZ: graficar latencia de A→B por AZ del caller y del target (TargetResponseTime del ALB por AZ; métricas custom con dimensión AZ). Hallazgo típico: solo las llamadas que cruzan de AZ-a a AZ-c sufren, o solo un target concreto está lento. (3) VPC Flow Logs (con Athena): buscar retransmisiones no se ve directamente, pero sí volúmenes anómalos, rechazos y pares origen-destino; para TCP fino, usar métricas de la instancia (TCPRetransSegs vía CloudWatch agent) o ENA metrics (bw_in_allowance_exceeded, pps_allowance_exceeded, conntrack_allowance_exceeded — instancias pequeñas agotan su asignación de red y ENA las estrangula: causa clásica de picos intermitentes). (4) Conexiones: ¿A crea conexiones nuevas por request (sin keep-alive) y el handshake+TLS explica el p99? ¿El pool de A se agota y las requests esperan pool? (métricas del cliente HTTP). ¿DNS? — resolvers con caché corta y lookup lento añaden decenas de ms intermitentes (usar keep-alive y cachear resolución). (5) Un solo target enfermo: CPU steal, GC pauses, EBS burst balance agotado, o un host degradado (status checks); el ALB reparte round robin y “salpica” el p99 con 1 de N targets lento — mirar métricas por target, no agregadas.
Soluciones según causa: cross-AZ dominante → activar zone-aware routing: en EKS, Topology Aware Hints/routing; en ECS Service Connect, preferencia zonal; con ALB, cross-zone on es el default (uniformidad) pero para latencia extrema se puede deshabilitar y alinear; también revisar que el cliente use los endpoints de su AZ. ENA allowances → instancia mayor o repartir carga. Pools → keep-alive con maxIdleTime < idle timeout del ALB (60 s default; el clásico race produce 502/latencia: el cliente reusa una conexión que el ALB acaba de cerrar), pool dimensionado al pico, y timeouts + retry con jitter solo en operaciones idempotentes. Target enfermo → health checks más sensibles (umbral outlier), drenarlo y reemplazarlo.
Prevención: SLO de latencia con métricas p99 por AZ y por target en dashboards permanentes; ENA metrics y conntrack en alarmas; pruebas de caos de red (FIS con latencia inyectada) para validar timeouts; y en diseño, presupuesto de latencia explícito por hop — una cadena síncrona de 4 servicios con 1 ms de cross-AZ por salto y p99 mal controlado en cada uno multiplica: el p99 compuesto es mucho peor que el de cada pieza (efecto fan-out/tail latency).
6. Mensajes de SQS que “reaparecen” y se procesan varias veces
Sección titulada «6. Mensajes de SQS que “reaparecen” y se procesan varias veces»Categoría: Integración · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Los consumidores reportan pedidos duplicados: el mismo mensaje se procesa 2-3 veces. Causa casi segura: el procesamiento excede el visibility timeout y SQS reentrega antes del delete, o el delete falla/no se llama en algún camino de código. Diagnóstico con la métrica de recepciones por mensaje (ApproximateReceiveCount) y comparando duración de procesamiento vs visibility timeout. Solución: visibility timeout correcto o heartbeat, delete garantizado, y —siempre— idempotencia, porque at-least-once hace que los duplicados nunca sean cero.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: el servicio de facturación consume una cola SQS estándar; contabilidad detecta facturas duplicadas. Los logs muestran el mismo messageId procesado a las 10:00:00 y a las 10:00:35.
Diagnóstico paso a paso: (1) Loggear y graficar ApproximateReceiveCount de cada mensaje recibido: si los duplicados llegan con count=2,3, es reentrega, no mensajes distintos con el mismo contenido (eso apuntaría al productor). (2) Medir la duración real de procesamiento (histograma) y compararla con el visibility timeout de la cola: aquí, el timeout es 30 s (default) y el p95 de procesamiento es 42 s (una llamada al PDF generator se degradó) → el mensaje vuelve a ser visible mientras el primer consumidor sigue trabajando; ambos terminan y facturan dos veces. (3) Revisar caminos de fallo del delete: ¿se llama DeleteMessage solo en el happy path? ¿Una excepción después del efecto pero antes del delete deja el mensaje vivo? ¿El batch delete parcial (DeleteMessageBatch con fallos por item) se verifica? (4) Con Lambda como consumidor: ¿ReportBatchItemFailures está activado? Sin él, un item fallido en un batch de 10 reprocesa los 10 (nueve duplicados por cada fallo). (5) Descartar productor duplicando: ¿el publisher reintenta sin idempotencia tras timeouts del SendMessage que en realidad llegaron? (los logs del SDK con request IDs lo revelan).
Solución: (1) Visibility timeout ≥ 6× el timeout/duración máxima de procesamiento (para 60 s de peor caso, poner ≥360 s), o mejor, heartbeat: extender con ChangeMessageVisibility periódicamente mientras se procesa (los frameworks tipo Spring Cloud AWS o el SQS Java Messaging Library lo traen). (2) Garantizar el delete en un finally correcto solo tras confirmar el efecto (nunca borrar antes de procesar: eso convierte duplicados en pérdidas). (3) Idempotencia obligatoria: clave messageId o clave de negocio (invoice-{orderId}) registrada en DynamoDB con escritura condicional attribute_not_exists y TTL; si la condición falla, saltar el procesamiento. Con esto, las reentregas legítimas de SQS (que ocurren incluso sin bugs: es at-least-once por diseño) dejan de tener efecto. (4) Arreglar la degradación del PDF generator (causa raíz del estiramiento de tiempos) y poner timeout propio menor que el visibility restante.
Prevención: alarma sobre ApproximateReceiveCount elevado (metric filter sobre logs, o revisar mensajes que llegan a DLQ con count alto), dashboards de duración de procesamiento vs visibility timeout, test de integración que simula el crash post-efecto y verifica que el reproceso no duplica, y la regla de arquitectura comunicada al equipo: todo consumidor SQS/SNS/EventBridge/Kinesis es idempotente por contrato, sin excepciones. En entrevista subrayo la trampa conceptual: la gente busca “el bug que duplica” cuando la duplicación es la semántica normal del sistema; el bug es el consumidor no idempotente.
7. La DLQ se está llenando: triage y reproceso
Sección titulada «7. La DLQ se está llenando: triage y reproceso»Categoría: Integración / Operación · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Una DLQ que crece es un síntoma con tres familias de causa: bug nuevo del consumidor (todos los mensajes fallan), poison messages (un subconjunto malformado) o dependencia caída/throttled (fallos transitorios que agotaron reintentos). Triage: muestrear mensajes de la DLQ sin consumirlos destructivamente, clasificar por tipo de error con los logs, corregir la causa y usar DLQ redrive para devolverlos a la cola origen a ritmo controlado. Prevención: alarma en DLQ>0, clasificación de errores en el consumidor y retención máxima de 14 días.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: alarma (ojalá exista) de ApproximateNumberOfMessagesVisible > 0 en la DLQ del servicio de notificaciones; en una hora hay 40,000 mensajes y subiendo.
Diagnóstico paso a paso: (1) ¿Qué porcentaje del tráfico muere? Comparar ritmo de entrada a la DLQ vs tráfico de la cola principal: 100% → bug de deploy reciente o dependencia dura caída; 0.1% → poison messages o un subconjunto de datos. (2) Muestrear la DLQ: recibir algunos mensajes con visibility corto (o mejor, una Lambda de inspección que copie a S3 sin borrar) y mirar atributos: ApproximateReceiveCount, timestamps, y el payload. (3) Correlacionar con logs del consumidor: buscar el error por messageId — ¿NullPointerException en un campo nuevo (bug tras cambio de contrato del productor)?, ¿ThrottlingException de una API downstream?, ¿timeouts a la BD? (4) Timeline: ¿cuándo empezó? Cruzar con deploys del consumidor Y del productor (un productor que cambió el esquema rompe consumidores sin tocar su propio código), y con incidentes de dependencias (status de terceros, métricas de RDS). (5) Verificar la configuración que aceleró el desastre: maxReceiveCount=1 manda transitorios a DLQ sin reintentar; visibility timeout demasiado corto multiplica receives.
Solución por familia: (a) Bug del consumidor → rollback o hotfix; los mensajes en DLQ son válidos: tras el fix, DLQ redrive (función nativa de SQS) hacia la cola origen con velocidad controlada para no aplastar al consumidor recién recuperado (y vigilar que la cola principal no tenga ya un backlog que sumado al redrive cause otra avalancha). (b) Poison messages → no re-driven: se procesan aparte (corregir datos y reinyectar transformados, o descartar documentadamente); el consumidor gana validación temprana que en el futuro los rechace a una cola de rechazo específica sin quemar reintentos. (c) Dependencia caída → esperar/mitigar la dependencia; los mensajes en DLQ se re-driven después; el consumidor gana circuit breaker + backoff para que una caída downstream no drene la cola entera hacia la DLQ (con el circuito abierto, mejor dejar de consumir — parar el event source mapping o escalar a 0 — que consumir y fallar: los mensajes esperan en la cola principal sin quemar receives).
Prevención: alarmas en DLQ (mensajes > 0, y edad del más antiguo), retención de la DLQ en 14 días (máximo margen), clasificación de errores en el consumidor (retryable vs no-retryable, con rutas distintas), runbook escrito de triage+redrive (esto es operación de madrugada: debe estar documentado), dashboards con la tasa de error por tipo, y contract testing entre productor y consumidor para eliminar la familia más común (cambios de esquema). Cierro con la idea fuerza: la DLQ no es un basurero, es una sala de urgencias — todo lo que entra se clasifica, se trata o se da de alta documentado; una DLQ ignorada durante 14 días es pérdida de datos silenciosa programada.
8. RDS PostgreSQL con CPU al 100%: análisis con Performance Insights
Sección titulada «8. RDS PostgreSQL con CPU al 100%: análisis con Performance Insights»Categoría: Bases de datos · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»CPU sostenida al 100% en RDS: abrir Performance Insights y mirar el load por wait event y las top SQL por AAS. Los sospechosos habituales: una query nueva sin índice (seq scan masivo), un ORM con N+1 desatado, estadísticas desactualizadas tras un cambio de datos, conexiones desbocadas (cada una cuesta memoria/CPU), o un autovacuum insuficiente que degrada todo. La solución combina matar/optimizar la query culpable, indexar, y controlar conexiones con RDS Proxy/pgbouncer; escalar la instancia es el último recurso, no el primero.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: la API degrada con timeouts; CPUUtilization de la instancia db.r6g.xlarge clavada al 100% desde hace 40 minutos. No hubo deploy de la app… aparentemente.
Diagnóstico paso a paso: (1) Performance Insights: gráfico de Average Active Sessions (AAS) por wait event — ¿CPU pura (verde) o waits (IO:DataFileRead = lecturas de disco por falta de índice/caché; Lock:* = contención; LWLock = contención interna)? Supongamos dominancia de CPU + IO:DataFileRead. (2) Pestaña Top SQL: una query nueva concentra el 70% del load. EXPLAIN (ANALYZE, BUFFERS) de esa query: seq scan sobre una tabla de 80M de filas — el deploy de ayer añadió un filtro por una columna sin índice (el “no hubo deploy” era mentira: fue un cambio de feature flag que activó un camino de código nuevo). (3) Confirmar el entorno: DatabaseConnections (¿se dispararon? — más conexiones = más procesos compitiendo; PostgreSQL sufre con miles de conexiones), pg_stat_activity para ver queries activas y su estado, pg_stat_user_tables para ver seq_scan creciendo y el estado de autovacuum (n_dead_tup alto = bloat que agrava todo). (4) Descartar causas alternativas rápidas: ¿burst balance de EBS agotado (gp2) o IOPS al límite? (BurstBalance, ReadIOPS vs provisioned); ¿un backup/replicación en curso?; ¿estadísticas obsoletas tras una carga masiva (ANALYZE pendiente) haciendo que el planner elija planes malos?
Solución inmediata: (1) mitigar: matar las queries culpables (pg_terminate_backend), desactivar el feature flag, o rate-limitear el endpoint que las dispara; (2) crear el índice correcto con CREATE INDEX CONCURRENTLY (sin lock de escrituras; no dentro de una transacción); (3) si el pool de conexiones estaba desbocado: imponer límites en la app y/o RDS Proxy (multiplexa; imprescindible con Lambda) o pgbouncer; (4) ANALYZE si las estadísticas estaban obsoletas. Solo si la carga legítima creció de verdad: escalar (Graviton r7g, o read replicas para desviar lecturas — con el matiz del lag para lecturas transaccionales).
Prevención: Performance Insights activo (7 días gratis) con alarma sobre AAS > vCPUs; pg_stat_statements habilitado y revisado (top queries por total_time en la retro semanal); en el pipeline, revisión de planes para queries nuevas (extensión de CI que corre EXPLAIN contra un esquema representativo) y regla de “toda columna filtrada en producción tiene índice o justificación”; presupuesto de conexiones documentado (max_connections se deriva de la memoria; cada conexión PostgreSQL son varios MB); autovacuum tuneado para tablas calientes (scale factors por tabla); y feature flags tratados como deploys en el timeline de incidentes (quedó demostrado). El antipatrón que nombro explícitamente: responder al 100% de CPU escalando la instancia sin diagnóstico — pospone el incidente una semana y duplica el costo.
9. API Gateway devolviendo 429 y 504: throttling y timeouts
Sección titulada «9. API Gateway devolviendo 429 y 504: throttling y timeouts»Categoría: Integración / Performance · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»429 = throttling: puede ser el límite de cuenta (10,000 RPS compartidos por región), el del stage/método, un usage plan, o —más sutil— el throttling de Lambda downstream propagado. 504 = integración que excede el timeout (29 s máx) o falla la conexión al backend. Diagnóstico con las métricas de API Gateway (Count, 4XXError, 5XXError, Latency vs IntegrationLatency) y logs de acceso, separando qué capa satura. Solución: cuotas correctas por cliente, límites elevados donde toque, y arreglar la latencia del backend o hacerlo asíncrono.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: clientes de la API pública reportan errores intermitentes 429 y algunos 504 en horas pico.
Diagnóstico de los 429 paso a paso: (1) Métricas por API/stage/método: ¿el Count total de la cuenta en la región roza 10,000 RPS? El límite de cuenta es compartido entre todas las APIs de la región — otra API interna con un cliente desbocado puede estar consumiendo el presupuesto de todos (este es el hallazgo no obvio que un senior busca pronto). (2) ¿Hay usage plans con API keys? Un cliente concreto recibiendo 429 con tráfico global bajo apunta a su cuota individual (correcto y deseable) — verificar en access logs ($context.identity.apiKey, $context.error.responseType=THROTTLED). (3) ¿El 429 viene de Lambda, no de API Gateway? Si la función agotó la concurrencia (límite de cuenta 1,000 o su reserved concurrency), API Gateway propaga el error; mirar Throttles de la función. (4) Access logs con $context.error.* distinguen exactamente el origen del throttle.
Diagnóstico de los 504: comparar Latency (total) vs IntegrationLatency (backend): si IntegrationLatency ≈ 29,000 ms, el backend excede el timeout máximo de 29 s (default 30 s efectivo en 29 para REST); con X-Ray, localizar el paso lento (query, dependencia externa). 504 con IntegrationLatency baja = fallo de conexión al backend (VPC Link caído, NLB sin targets sanos, DNS).
Soluciones: para 429 — (1) subir el límite regional vía Service Quotas si el tráfico agregado legítimo creció (es soft); (2) poner usage plans con cuotas por cliente si no existen: sin ellos, un cliente ruidoso agota el pool de todos (fairness); (3) para el throttling de Lambda: subir el límite de cuenta, reserved concurrency para la función crítica, y colas para aplanar picos; (4) en el cliente, retry con backoff exponencial + jitter respetando Retry-After — y documentarlo en el contrato de la API. Para 504 — optimizar el backend (la mayoría de 504 son una query o un downstream lento), o rediseñar a asíncrono: si una operación tarda >29 s por naturaleza (informe, procesamiento), el patrón correcto es 202 Accepted + polling o webhook/WebSocket de notificación, no estirar timeouts. Considerar caché de API Gateway para los GET calientes (baja Count y latencia a la vez).
Prevención: alarmas sobre 4XX/5XX rate y sobre el consumo del límite regional (métrica Count agregada de la cuenta vs 10,000), separación de APIs críticas en cuentas distintas (el límite compartido es un blast radius real), usage plans desde el día uno en APIs públicas, load testing que incluya el escenario “cliente ruidoso”, y presupuesto de latencia del backend < 5-10 s con el timeout de integración ajustado (fail fast) en lugar del default. Meta-lección: API Gateway es un sistema de límites anidados (cuenta → stage → método → usage plan → backend); diagnosticar 429/504 es identificar qué capa habló.
10. Caída de una AZ: comportamiento del sistema y diseño de resiliencia
Sección titulada «10. Caída de una AZ: comportamiento del sistema y diseño de resiliencia»Categoría: Arquitectura / Resiliencia · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Se degrada una AZ: lo que se espera de un sistema bien diseñado es pérdida de ~1/3 de capacidad absorbida por el headroom, failover automático de RDS/Aurora en segundos-minutos, y ALB/ASG sacando de servicio los targets enfermos — sin intervención humana. El caso evalúa: detección (¿cómo sabes que es la AZ?), respuesta (evacuar la AZ, no arreglarla), y diseño previo (static stability, sin dependencias zonales ocultas, datos multi-AZ). La trampa clásica: componentes “grises” que fallan a medias y health checks que no lo detectan.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: 03:20 AM, sube la tasa de errores al 8% y la latencia p99 x4. Ninguna alarma dice “AZ caída” (AWS tarda en reconocerlo en su Health Dashboard); solo hay síntomas dispersos.
Detección paso a paso: (1) Dashboards con dimensión de AZ: tasa de error y latencia por AZ del target — si el 90% de los errores vienen de targets en use1-az4, el patrón es zonal (por esto todas las métricas deben llevar AZ como dimensión desde el diseño). (2) UnHealthyHostCount del ALB por AZ; eventos de ECS/EKS de tasks/pods fallando concentrados en nodos de esa AZ; errores de conexión a ENIs/EBS de esa AZ en logs. (3) AWS Health Dashboard y el feed de status — llegará tarde; no esperar confirmación oficial para actuar. (4) Cuidado con el gray failure: la AZ no muere del todo — pierde el 30% de paquetes o degrada EBS; los health checks superficiales (TCP al puerto) pasan mientras las requests reales fallan: aquí valen los health checks profundos y las métricas de negocio.
Respuesta: el principio es evacuar, no diagnosticar la AZ: (1) si el auto-healing no lo hizo ya, usar zonal shift (ARC — Route 53 Application Recovery Controller): aparta la AZ del ALB/NLB en segundos sin tocar infra (o deshabilitar la subnet en el balanceador); (2) verificar que el ASG/ECS/EKS compensa capacidad en las AZs sanas — aquí se cobra la static stability: si dimensionaste N+1 (capacidad para servir el 100% con una AZ menos), no dependes de lanzar instancias durante el incidente, cuando las APIs de control están saturadas por toda la región intentando lo mismo; (3) bases de datos: Aurora promociona réplica en ~30 s automáticamente; RDS Multi-AZ failover 60-120 s; verificar que las apps reconectan (los pools con DNS cacheado son el clásico: el failover ocurrió pero la app sigue apuntando a la IP vieja — TTLs bajos y validación de conexiones en el pool); (4) verificar colas y consumidores (SQS es regional, no le pasa nada; los consumidores en la AZ muerta los reemplaza el orquestador); (5) comunicar y registrar timeline.
Diseño previo que hace este incidente aburrido: 3 AZs con headroom para perder una (o sobredimensionamiento 50% con 2… por eso 3 AZs: el headroom cuesta 50% con 2 AZs y 33% con 3); cero recursos zonales únicos (un NAT por AZ, nada crítico en una sola subnet, EBS es zonal — los StatefulSets de EKS anclados a una AZ necesitan plan); datos siempre multi-AZ (Aurora, DynamoDB, S3, EFS); colas regionales entre servicios (absorben la turbulencia del failover); y game days con FIS simulando la pérdida de AZ (aws:network-disruption sobre una AZ) al menos semestralmente — la diferencia entre “creemos que somos multi-AZ” y demostrarlo. Cierro con la métrica de éxito: una caída de AZ en un sistema bien diseñado debe ser una anécdota de guardia (páginas de runbook ejecutadas, cero decisiones creativas), no un incidente mayor.
11. Fuga de credenciales IAM: respuesta al incidente
Sección titulada «11. Fuga de credenciales IAM: respuesta al incidente»Categoría: Seguridad · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Un access key aparece en un repo público / GuardDuty alerta de uso anómalo. Respuesta inmediata: contener (desactivar la key, aplicar deny-all temporal, revocar sesiones activas del rol), evaluar (CloudTrail: qué hizo la identidad desde la exposición — datos, persistencia, escalada), erradicar (rotar todo lo alcanzado, borrar recursos backdoor) y recuperar. Después: root cause (cómo llegó la key ahí) y prevención estructural: eliminar access keys estáticas en favor de roles, secret scanning en CI, y GuardDuty/Access Analyzer activos.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: GitGuardian (o AWS, que escanea repos públicos y abre un caso de soporte automático aplicando la policy AWSCompromisedKeyQuarantineV2) notifica que el access key AKIA… de un “usuario de servicio” está en un commit público desde hace 6 horas. GuardDuty muestra IAMUser/AnomalousBehavior y llamadas desde IPs desconocidas.
Contención (minutos): (1) Desactivar el access key (no borrarlo aún: el ID sirve para la investigación) desde una identidad segura; (2) si la credencial es de un rol (session tokens robados de una instancia/Lambda), desactivar la key no aplica: se usa revocación de sesiones (la política AWSRevokeOlderSessions inline en el rol invalida tokens emitidos antes de ahora) y se rota el origen (reciclar la instancia); (3) aplicar una deny-all o quarantine policy al principal mientras se investiga; (4) preservar evidencia: no borrar nada todavía; snapshot del estado IAM del principal (policies adjuntas, ¿cambiaron?).
Evaluación con CloudTrail (horas): consultar (Athena sobre el trail, o CloudTrail Lake) todas las llamadas del access key ID desde el primer uso sospechoso: userIdentity.accessKeyId = 'AKIA…'. Buscar en orden: (1) reconocimiento: GetCallerIdentity, ListBuckets, ListUsers, DescribeInstances; (2) exfiltración: GetObject masivos (¡ojo: data events de S3 deben estar habilitados en el trail o no los verás!), GetSecretValue, snapshots compartidos a otra cuenta (ModifySnapshotAttribute), exports; (3) persistencia/escalada: CreateUser, CreateAccessKey sobre otros usuarios, CreateRole/UpdateAssumeRolePolicy (backdoor de trust policy), AttachUserPolicy con AdministratorAccess, CreateLoginProfile, Lambdas o instancias nuevas con roles potentes, cambios en bucket policies; (4) uso de recursos: minado (instancias GPU en regiones raras — revisar TODAS las regiones), envío de spam por SES. Cada hallazgo amplía el perímetro: todo lo que la identidad pudo tocar se considera comprometido.
Erradicación y recuperación: borrar toda persistencia encontrada (usuarios, keys, roles backdoor, funciones); rotar todos los secretos alcanzables por la identidad (Secrets Manager, contraseñas de BD, API keys de terceros); si tocó datos: proceso de brecha (legal, notificaciones); restaurar recursos alterados desde IaC (drift detection ayuda a ver qué cambió); finalmente borrar la key y, idealmente, el usuario IAM entero.
Prevención estructural: (1) cero access keys estáticas: roles para todo (IRSA, task roles, instance profiles, GitHub OIDC para CI — el caso típico de key filtrada es justamente la de CI); (2) secret scanning pre-commit y en CI (gitleaks, trufflehog) más push protection de GitHub; (3) GuardDuty en todas las cuentas/regiones con enrutamiento a on-call real; (4) CloudTrail organizacional inmutable (a un bucket en cuenta de log-archive con Object Lock) con data events de S3 en buckets sensibles; (5) least privilege real (la diferencia entre “filtré una key con SendMessage a una cola” y “filtré una key con AdministratorAccess” es la diferencia entre un susto y una brecha); (6) SCPs que nieguen regiones no usadas (limita el minado); (7) runbook de este incidente escrito y ensayado — es de los pocos que garantizado ocurrirá.
12. S3 lento en listados masivos y cargas con millones de objetos
Sección titulada «12. S3 lento en listados masivos y cargas con millones de objetos»Categoría: Almacenamiento / Performance · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Un job nocturno que lista un bucket con 200M de objetos tarda horas y a veces recibe 503: ListObjectsV2 pagina de 1,000 en 1,000 secuencialmente y no escala. El diagnóstico confirma que el cuello es el listado, no las descargas. Solución: dejar de listar — S3 Inventory para el catálogo diario, notificaciones de eventos para lo incremental, o un índice propio en DynamoDB; y paralelizar por prefijos cuando el listado sea inevitable. Los 503 en PUT masivos se tratan con reparto por prefijos y retries con backoff.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: un batch de reconciliación lista todo el bucket (s3://data/eventos/…, 200M de objetos) para encontrar los archivos del día, y luego los procesa. Tarda 4-6 horas, la mayoría en el listado, con 503 SlowDown ocasionales que rompen el job.
Diagnóstico paso a paso: (1) Instrumentar el job: ¿cuánto tiempo va a ListObjectsV2 vs GetObject? Con 200M de objetos a 1,000 keys por página son 200,000 llamadas secuenciales (cada página necesita el continuation token de la anterior): a ~100 ms por llamada, 5.5 horas solo listando — matemática condenatoria. (2) CloudWatch request metrics del bucket (activarlas por filtro): 5xxErrors, latencias FirstByteLatency/TotalRequestLatency — los 503 aparecen en ráfagas de PUT/LIST concentradas (el particionado interno de S3 se adapta al tráfico, pero los picos súbitos sobre un prefijo frío disparan SlowDown transitorios mientras escala; 3,500 PUT y 5,500 GET/s por prefijo es el baseline). (3) Server access logs o CloudTrail data events para ver la distribución por prefijo: todo el tráfico cae en eventos/2026/08/10/ — un único prefijo caliente.
Solución: (1) Eliminar el listado del camino: la pregunta “¿qué objetos hay?” no se responde listando en runtime sino con S3 Inventory (entrega diaria/semanal del catálogo completo en Parquet a otro bucket; Athena lo consulta en segundos) para reconciliación batch, y S3 Event Notifications → SQS/EventBridge para lo incremental (el job ya no busca los archivos del día: los archivos se anuncian solos al llegar, y el batch se convierte en streaming o en un catálogo DynamoDB actualizado por eventos). (2) Si hay que listar: paralelizar por prefijos disjuntos (Prefix + Delimiter por hora/shard: 24 listados en paralelo) — requiere que el esquema de keys reparta: eventos/2026/08/10/17/… o mejor con un shard hash eventos/3f/2026/… si el volumen por hora también es enorme. (3) Para los 503: retries con backoff exponencial + jitter (el SDK lo trae, ajustar maxRetries), y repartir escrituras masivas entre múltiples prefijos desde el diseño de keys. (4) Revisar si el patrón entero pertenece a otra herramienta: catálogos de datos analíticos sobre S3 son el caso de uso de S3 Tables/Iceberg (metadata transaccional, sin listados) o de Glue Catalog + particiones.
Prevención: diseño de keys pensado para el paralelismo (fecha al final o shard delante si el tráfico se concentra), S3 Inventory activado en buckets grandes desde el inicio (es barato: $0.0025/M de objetos listados), notificaciones de eventos como fuente de verdad incremental, y una regla de revisión de arquitectura: ningún proceso en el request path o con SLA depende de listar un bucket — listar es una operación de catálogo, y los catálogos se mantienen, no se recalculan. Menciono también el límite práctico: ListObjectsV2 no tiene orden por fecha ni filtros por metadata — cualquier necesidad así ya es un índice externo por definición.
13. Kinesis con shards saturados y consumidores con lag creciente
Sección titulada «13. Kinesis con shards saturados y consumidores con lag creciente»Categoría: Streaming / Performance · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Productores recibiendo ProvisionedThroughputExceededException y consumidores con IteratorAgeMilliseconds creciendo: el stream no da abasto o está mal repartido. Diagnóstico con métricas enhanced a nivel de shard para distinguir saturación global (todos los shards al límite → faltan shards) de hot shard (una partition key dominante → problema de clave). Solución: resharding o on-demand para lo global, rediseño de partition key para lo local, y en consumo, más paralelismo (enhanced fan-out, parallelization factor) o procesamiento más rápido.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: pipeline de clickstream con Kinesis Data Streams (32 shards, provisioned) → Lambda. Desde ayer, los productores loguean throttling intermitente y el dashboard muestra GetRecords.IteratorAgeMilliseconds subiendo hasta 20 minutos: los consumidores procesan datos cada vez más viejos.
Diagnóstico paso a paso: (1) Separar los dos problemas: throttling de escritura (productor) y lag de lectura (consumidor) pueden tener causas independientes. (2) Enhanced shard-level metrics (activarlas: IncomingBytes, IncomingRecords, WriteProvisionedThroughputExceeded por shard): ¿el throttling se concentra en 2-3 shards (hot shard) o es uniforme (capacidad global)? Aquí: 3 shards concentran el 60% del tráfico → partition key sesgada (el key era siteId y dos sites dominan). (3) Lag de consumo: ¿IteratorAge crece en todos los shards o en los calientes? ¿La Lambda tarda más (Duration subió tras el último deploy — una llamada nueva a una API externa de 200 ms por batch)? ¿Errores y reintentos (bisectBatchOnFunctionError partiendo batches, cada mitad re-procesada)? ¿Cuántos consumidores comparten los 2 MB/s por shard (polling compartido + otra app nueva leyendo el mismo stream = mitad de throughput para cada una)? (4) Revisar el productor: ¿usa PutRecords con batching y maneja los fallos parciales del batch (los records rechazados hay que reintentarlos explícitamente — el error clásico silencioso: PutRecords devuelve 200 con FailedRecordCount>0 y nadie lo mira → pérdida de datos)?
Solución: (1) Hot shard → rediseñar la partition key: siteId#hash(sessionId)%N para repartir los sites gigantes conservando orden por sesión (el orden global por site se pierde: negociar qué orden importa de verdad); mientras se despliega, un split de los shards calientes alivia (split/merge son por shard, dirigidos). (2) Capacidad global → resharding a 64, o cambiar a on-demand si el tráfico es impredecible (absorbe hasta 2× el pico previo automáticamente; más caro por GB, pero elimina esta clase de incidente). (3) Lag de consumo → optimizar la Lambda (esa llamada externa: cachearla o sacarla del camino), subir ParallelizationFactor (hasta 10 invocaciones concurrentes por shard manteniendo orden por partition key), batch size mayor con MaxBatchingWindow, y si hay múltiples consumidores, enhanced fan-out (2 MB/s dedicados por consumidor, push, ~70 ms). (4) Backlog acumulado: con más paralelismo el sistema drena; calcular horas de drenado = backlog / (throughput de proceso − ingesta) y decidir si se tolera o se escala más agresivamente; verificar que la retención (¿24 h?) cubre el drenado — si el lag amenaza la retención, subirla YA (hasta 7-365 días) o se pierden datos.
Prevención: alarmas sobre IteratorAge (el SLO de frescura del pipeline), WriteProvisionedThroughputExceeded y FailedRecordCount del productor; shard-level metrics activas en streams críticos; test de distribución de la partition key con tráfico real ante del diseño (cardinalidad y sesgo); dimensionamiento documentado (shards = pico MB/s ÷ 1 con margen ×1.5); y revisión del fan-out de consumidores al añadir cada lector nuevo. Cierro señalando el trade-off de fondo: Kinesis acopla orden y paralelismo a la partition key — elegirla es la decisión de diseño, todo lo demás es tuning.
14. Migración a AWS: lift-and-shift vs re-architect
Sección titulada «14. Migración a AWS: lift-and-shift vs re-architect»Categoría: Arquitectura / Estrategia · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Monolito on-premises con fecha de salida del datacenter: la decisión no es binaria ni global — se decide por aplicación con las 7R (rehost, replatform, refactor, repurchase, relocate, retain, retire). Mi enfoque: rehost/replatform primero para cumplir la fecha y ganar operabilidad básica (managed DB, backups, autoscaling básico), y refactor selectivo después, dirigido por dolor real (los módulos que más cambian o más cuestan), no por pureza arquitectónica. El lift-and-shift “temporal” que se vuelve permanente es el riesgo a gestionar con un plan de optimización comprometido.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: e-commerce con monolito Java + Oracle en datacenter propio cuyo contrato vence en 9 meses. El CTO pregunta: ¿migramos tal cual o rediseñamos a microservicios “ya que estamos”?
Análisis paso a paso: (1) Inventario y dependencias (discovery con Application Discovery Service o manual): qué apps, qué integraciones, qué SLAs, qué licencias (Oracle en EC2 tiene implicaciones de licencia serias — a veces fuerza el replatform a Aurora PostgreSQL o el retain). (2) Clasificación 7R por aplicación: el 60-70% típico va a rehost (VM → EC2, con MGN automatizando la réplica) o replatform (cambios sin tocar arquitectura: Oracle → RDS/Aurora, WebLogic → Tomcat en ECS, NFS → EFS); un puñado a repurchase (el CRM casero → SaaS), retire (el 10-20% de apps que nadie usa — la auditoría siempre encuentra), retain (el AS/400 que nadie tocará) y solo lo justificado a refactor. (3) La pregunta clave para refactor: ¿qué gana el negocio? Refactorizar a microservicios durante una migración con fecha dura suma dos riesgos enormes (migración + rediseño) con un equipo que aún no opera AWS; la tasa de fracaso de “big bang re-architecture” es altísima. Refactor se justifica cuando el monolito ya bloquea el delivery (equipos pisándose, deploys semanales de terror) o cuando el costo de rehost es absurdo (mainframe, licencias).
Plan que defendería: fase 1 (meses 1-6): landing zone (Organizations, cuentas, redes, seguridad, IAM federado — hacerlo bien aquí paga para siempre), rehost de la mayoría con MGN, replatform de la base de datos (Oracle → Aurora PostgreSQL vía DMS + SCT si la compatibilidad lo permite; es el replatform con mejor ROI por licencias) y del monolito a contenedores en ECS si el cambio es barato (mismo código, imagen Docker) — “replatform ligero”. Fase 2 (meses 7-9): estabilización, colchón para sorpresas, cutover con réplica continua (DMS CDC) y rollback plan. Fase 3 (post-fecha, comprometida con presupuesto): optimización dirigida por datos — con el sistema ya en AWS y observable, extraer los módulos con más cambio/escala (strangler fig: el checkout primero, detrás de un ALB con routing por path), Savings Plans, right-sizing con métricas reales.
Trade-offs y trampas que expongo: lift-and-shift puro suele costar 10-30% más que on-prem al inicio (VMs sobredimensionadas 24/7 sin los descuentos de compromisos) — el ahorro llega con la optimización, por eso la fase 3 debe tener presupuesto y dueño o no ocurrirá jamás (“lo temporal permanente”); re-architect total antes de migrar retrasa la salida y se diseña sin conocer AWS en producción; y la migración es el mejor momento para instaurar IaC, tagging y CI/CD (las cuentas nuevas nacen limpias) — replicar el caos on-prem en la nube es la oportunidad perdida más cara. La respuesta senior es cartera + fases + criterios económicos, nunca “microservicios porque sí” ni “todo tal cual porque hay prisa”.
15. ECS tasks que mueren por memoria (OOM) de forma intermitente
Sección titulada «15. ECS tasks que mueren por memoria (OOM) de forma intermitente»Categoría: Contenedores / ECS · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»Tasks de Fargate reinician esporádicamente con OutOfMemoryError: Container killed due to memory usage (exit code 137). Diagnóstico: stopped reason de las tasks, métricas de memoria (idealmente Container Insights, que da el detalle por contenedor), y análisis de si es fuga (crecimiento monótono hasta morir), picos legítimos (cargas grandes puntuales) o un límite de JVM/runtime mal alineado con el límite del contenedor. Solución según causa: alinear heap y límites, subir memoria de la task, arreglar la fuga, o acotar el tamaño de trabajo por request. Prevención: límites derivados de perfiles reales y alarmas de memoria antes del 100%.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: el servicio de generación de informes (Fargate, 1 vCPU / 2 GB, Java) pierde 3-5 tasks al día con exit 137; el ALB da 502 esporádicos durante los reemplazos.
Diagnóstico paso a paso: (1) Stopped reason: DescribeTasks de tasks paradas (o EventBridge capturando eventos ECS Task State Change a un log — las tasks paradas desaparecen de la consola en minutos; sin esta captura, se pierde la evidencia): OutOfMemoryError: container killed confirma OOM del cgroup, exit 137. (2) Perfil temporal: con Container Insights (MemoryUtilized por task/contenedor), ¿la memoria crece monótonamente durante horas hasta morir (fuga o caché sin límite) o salta de 60% a 100% en segundos (un request grande)? Correlacionar el momento de la muerte con logs: ¿siempre procesando el informe del cliente X (el CSV de 800 MB)? (3) Runtime vs contenedor: en Java, ¿qué heap máximo tiene la JVM? Sin flags, las JVM modernas toman 25% de la memoria detectada; con -Xmx1800m en una task de 2 GB no queda espacio para off-heap (metaspace, threads, buffers nativos, ~200-400 MB): el proceso excede el límite del cgroup y el kernel lo mata sin stack trace Java (por eso “no hay OOMException en los logs” — la muerte es externa). Regla: heap ≈ 50-75% del límite del contenedor (-XX:MaxRAMPercentage=65). (4) Revisar si hay sidecar (agente de observabilidad) compartiendo el límite de la task y creciendo.
Solución según diagnóstico: (a) Desalineación de límites → MaxRAMPercentage correcto y, en task definitions con múltiples contenedores, memoryReservation (soft) + memory (hard) por contenedor coherentes con el total de la task; (b) picos por requests grandes → primero acotar el trabajo (streaming del CSV en vez de cargarlo entero, paginación, límite de tamaño con 413 para lo inaceptable), y si el pico es legítimo, subir la task (Fargate llega a 120 GB) o derivar los informes pesados a un worker asíncrono (SQS + task dedicada grande) fuera del servicio interactivo; (c) fuga → heap dump antes de la muerte (-XX:+HeapDumpOnOutOfMemoryError solo captura OOM de heap; para la muerte por cgroup, tomar dumps periódicos o usar el perfil de crecimiento con JFR) y arreglarla; mientras tanto, un reciclado programado de tasks es un parche honesto. (d) Mitigar el impacto en usuarios: deregistration_delay y health checks bien puestos hacen que el reemplazo no genere 502 (el 502 delata que el ALB seguía enviando tráfico a una task muriendo: revisar draining).
Prevención: perfiles de carga con el peor caso real antes de fijar límites; alarma sobre memoria >80% sostenida por task (Container Insights) — el OOM nunca debe ser la primera señal; captura permanente de stopped reasons a logs; límites de tamaño en las entradas (uploads, batch sizes) como contrato; y en la revisión de diseño, la pregunta “¿qué pasa cuando llegue la entrada 10× más grande de lo normal?” — porque llegará. Nota senior: en Fargate no hay swap ni vecinos que ayuden; el límite es un contrato duro, y eso es bueno — hace el fallo reproducible y diagnosticable.
16. NAT Gateway con costos disparados: análisis y rediseño
Sección titulada «16. NAT Gateway con costos disparados: análisis y rediseño»Categoría: Networking / FinOps · Tipo: [CASO] Análisis de problema
📝 Respuesta resumen
Sección titulada «📝 Respuesta resumen»La línea NatGateway-Bytes aparece con miles de dólares: cada GB procesado cuesta $0.045 además de la transferencia. Diagnóstico: VPC Flow Logs con Athena agrupando el tráfico que atraviesa la ENI del NAT por destino — los culpables habituales son S3/ECR/DynamoDB (arreglable con endpoints), APIs externas de gran volumen, o tráfico entre VPCs mal enrutado. Solución: gateway endpoints (gratis), interface endpoints donde compensen, y revisar la arquitectura del tráfico masivo. Prevención: endpoints en el módulo estándar de VPC y monitoreo de bytes por NAT.
📖 Respuesta detallada
Sección titulada «📖 Respuesta detallada»Escenario: Cost Explorer muestra $9,000/mes en EC2-Other → NatGateway-Bytes, cuando hace tres meses eran $400. Nadie sabe qué pasa por ahí.
Diagnóstico paso a paso: (1) CloudWatch del NAT: BytesOutToDestination/BytesInFromSource por NAT Gateway — identifica cuál de los NATs (uno por AZ) procesa el grueso y desde cuándo (la fecha de inflexión se cruza con deploys). (2) VPC Flow Logs a S3 + Athena: filtrar los flows cuya interfaz es la ENI del NAT y agrupar por IP de destino y puerto: SELECT dstaddr, sum(bytes) FROM flows WHERE interface_id='eni-nat…' GROUP BY dstaddr ORDER BY 2 DESC. Resolver las IPs top: ¿rangos de S3/ECR de la propia región (los AWS ip-ranges.json los identifican)? ¿Una API de un tercero? ¿IPs de otra VPC propia? (3) Hallazgos típicos, por frecuencia real: pulls de ECR/S3 (cada deploy o cada task nueva baja gigas de imágenes; con autoscaling agresivo, terabytes al mes — y ECR usa S3 para las layers), datalakes (jobs leyendo/escribiendo S3 vía NAT), logs/métricas a un SaaS de observabilidad (agentes enviando TB a Datadog/Splunk), DynamoDB/otros servicios AWS sin endpoint, tráfico a otra VPC que debería ir por peering/TGW pero cae al NAT por rutas mal hechas, y algún bucle de reintentos descargando lo mismo sin caché.
Solución en orden de ROI: (1) Gateway endpoints de S3 y DynamoDB: gratis, media hora de trabajo, eliminan de golpe todo ese tráfico del NAT (en el caso típico, es el 60-80% del problema — el pull de ECR pasa sus layers por el endpoint de S3). (2) Interface endpoints para ECR (api y dkr), CloudWatch Logs, STS, Secrets Manager: ~$7.2/mes por AZ cada uno más $0.01/GB — se justifica si el tráfico de ese servicio supera unos cientos de GB/mes (el GB por endpoint cuesta 4.5× menos que por NAT). (3) Tráfico a terceros de gran volumen: no tiene endpoint posible; opciones: reducirlo (compresión y sampling de logs/telemetría — a menudo el 90% del volumen a Datadog es evitable), o si el tercero ofrece PrivateLink (muchos SaaS lo dan), consumirlo por ahí. (4) Rutas incorrectas entre VPCs: corregir route tables para que el tráfico interno vaya por peering/TGW, nunca por NAT + IPs públicas (además de caro, es peor en seguridad). (5) Considerar arquitectura: ¿los workers que procesan S3 masivamente pueden vivir en subnets con endpoint y sin pasar por NAT en absoluto? ¿El registry de contenedores puede tener pull-through cache local?
Prevención: los gateway endpoints y los interface endpoints básicos (ECR, Logs) forman parte del módulo estándar de VPC en IaC — ninguna VPC nace sin ellos; dashboard de BytesOutToDestination por NAT con alarma de tendencia; revisión de la línea NatGateway-Bytes en la retro mensual de costos; y en el checklist de diseño de todo servicio nuevo: “¿qué tráfico generará y por dónde saldrá?”. Remato con el patrón general: el NAT Gateway cobra por ser la salida por defecto; una arquitectura madura convierte cada flujo de gran volumen en una ruta específica y barata (endpoint, PrivateLink, peering), dejando al NAT solo el tráfico residual a internet.