← blogENhome

AWS · Troubleshooting · 2026-09-03 · 6 min de lectura

Una imagen de 599 MiB, 91 minutos y la región de al lado

Un deploy en el cluster Kubernetes on-premise de un cliente estaba clavado en ImagePullBackOff. La imagen — un build de Node.js de 599 MiB alojado en Amazon ECR — eventualmente bajó. Tardó 1 hora, 31 minutos y 30 segundos.

El patrón

Las transferencias TCP sostenidas desde la IP de egreso del datacenter hacia ECR en us-east-2 morían en pleno vuelo tras transferir entre 1,3 y 3,1 MB, de forma intermitente, con una tasa de fallo de entre 40% y 83%. Reproducible desde tres segmentos de red internos distintos, en un servidor sin runtime de contenedores, con curl a secas. El único factor común era la IP pública de salida.

El gradiente por región era la parte interesante: us-east-2 fallaba el 40–83% de las veces, us-west-2 alrededor del 20%, y us-east-1 no fallaba nunca. Esa forma descarta una política que apunte a "AWS" como conjunto, y la reproducción desde segmentos internos independientes descarta una regla de firewall por segmento. Todo apuntaba a degradación de tránsito hacia los prefijos de esa región específica — en algún punto entre el borde del datacenter y el carrier. En otras palabras: una causa raíz que no era nuestra y no podíamos arreglar.

Por qué las imágenes grandes sufren desproporcionadamente

El corte ronda los 2,5 MB por sesión TCP, y las imágenes de contenedor no se transfieren como un solo stream — se transfieren por capa. Las capas por debajo del umbral casi nunca fallan. La imagen de 599 MiB tenía 4 capas por encima (223, 197, 149 y 29 MiB).

Con aproximadamente un 60% de probabilidad de que una capa grande complete en un intento dado, la probabilidad de que las cuatro pasen en una sola ronda es de 0,6⁴ ≈ 13%. containerd reintenta por capa, así que el pull converge eventualmente — pero cada fallo desperdicia hasta 223 MiB de transferencia y el backoff se acumula. Eso son los 91 minutos completos, sin necesidad de ningún modo de falla exótico.

También explica por qué el fix de Dockerfile del equipo de la app — que bajó la imagen de 599 a 279 MiB, un 53% — no resolvió nada: dejó las mismas cuatro capas por encima del umbral. Achicar el total reduce el costo de cada fallo, no la cantidad de tiradas de dado. Lo que importa acá no es el tamaño de la imagen; es la distribución de tamaños de capa.

¿No podés arreglar el camino? Mové el endpoint

Como us-east-1 estaba limpia, el workaround fue servir la imagen desde ahí: replicación cross-region de ECR más el sembrado. Cada paso tenía su trampa:

Las trampas de medición

Dos cosas casi producen conclusiones falsas, y valen más que el fix en sí:

El caché de containerd hace mentir a cualquier test de pull entre regiones. Como los digests se preservan, un nodo que ya tiene las capas de la región A "pullea" desde la región B en menos de un segundo, sin transferir nada. Un test temprano reportó 334 MB en 796 ms — 420 MB/s, físicamente imposible por ese enlace. Antes de medir: borrar la imagen, confirmar vía .status.images del kubelet que el nodo está realmente frío, y recién ahí pullear. Forzamos el scheduling al único nodo que nunca había tenido la imagen.

Un 401 del registry es invisible para CloudTrail. El pull fallido de us-east-1 nunca apareció como evento de API — porque un request no autenticado al endpoint /v2/ del registry nunca llega a ser una llamada autenticada a la API de AWS. Durante el diagnóstico, "CloudTrail está vacío" se leyó por un momento como "nadie intentó pullear", cuando es exactamente lo que predice un fallo de credencial. Hay que saber qué fallas tu telemetría no puede ver.

Trampa extra: iam simulate-principal-policy devolvió implicitDeny para todas las acciones de los dos principals — incluidas acciones explícitamente permitidas sobre Resource: "*". La pista era MatchedStatements: []: el simulador no había cargado ninguna policy (permisos de lectura insuficientes) y reporta eso como deny en vez de error. Lo contradecía CloudTrail, que mostraba a las dos identidades pulleando y pusheando activamente. Cuando un simulador y la realidad no coinciden, creele a la realidad.

Resultado

Misma imagen, mismo digest, mismo nodo, cold pull real:

us-east-2:  1h 31m 30s   (~3,9 MB/s, 40–83% de sesiones cortadas, muchos reintentos)
us-east-1:  1m 22s       (7,6 MB/s, cero reintentos)

67× más rápido. Con la salvedad honesta de que un pull limpio es n=1 — us-east-2 completaba 6 de 10 intentos en las mediciones originales, así que un pull bueno desde ahí también es posible. Pero 82 segundos contra 5.490 no es varianza; es la diferencia entre un pase limpio y una pila de backoffs. Quedó un arnés de medición de cuatro celdas (2 tags × 2 regiones, intercaladas, clasificando por código de salida) para quien quiera intervalos de confianza.

Y es un workaround, no un fix: los runners de CI siguen pusheando por el camino degradado, y la escalación real — al equipo de redes y al carrier dueño del prefijo de egreso — es donde vive la causa raíz.

Lecciones

ECRcontainerdKubernetesAWSnetworking