Imagina que eres el gerente de un supermercado. Es sábado por la mañana, tienes 2 cajas abiertas y el ritmo es tranquilo. Los clientes entran, pagan, se van. Nadie espera más de un minuto. Tus cajeros están relajados, tú estás relajado. El mundo funciona.
Sábado 10am
Pocas personas, 2 cajas abiertas. Todo fluye sin problema.
cliente siendo atendido caja libre caja inactiva
Llegan las 6 de la tarde y el supermercado se transforma. Es quincena, la gente sale del trabajo y cae toda al mismo tiempo. De repente tienes filas de 8, 10 personas en cada caja. Los clientes se miran entre ellos, miran el reloj, y algunos directamente dejan el carrito y se van. Estás perdiendo ventas frente a tus ojos. Entonces haces lo que cualquier gerente haría. Entras en pánico, llamas a más empleados y abres todas las cajas que puedes.
Sábado 6pm
Hora pico: llegan muchos clientes. Las filas crecen.
en fila siendo atendido caja libre caja inactiva
Funcionó. Las filas bajan, los clientes dejan de irse y respiras aliviado. Pero te queda el susto. Piensas "no voy a dejar que esto vuelva a pasar". Así que decides dejar las 6 cajas abiertas el resto de la noche. El problema es que a las 11pm el supermercado se ve así.
Sábado 11pm
El rush paso. 6 cajas abiertas, 2 clientes. Estas pagando por cajas que nadie usa.
Seis cajeros cobrando sueldo para atender a dos personas. Uno bosteza, otro revisa el celular, y tú estás pagando por capacidad que nadie necesita. El problema nunca fue tener pocas cajas ni tener muchas. El problema era no poder ajustar la cantidad en tiempo real según la demanda. Abrir cajas cuando la fila crece y cerrarlas cuando el flujo baja. Ni más ni menos de lo que necesitas, justo cuando lo necesitas.
En el mundo de los servidores pasa exactamente lo mismo. Tu aplicación corre en máquinas que atienden requests como un cajero atiende clientes. Si llegan más requests de lo que las máquinas pueden procesar, los usuarios ven errores o tiempos de espera eternos. Podrías tener 20 máquinas corriendo todo el tiempo por si acaso, pero estarías pagando una fortuna por máquinas que no hacen nada el 90% del tiempo.
La solución se llama autoscaling. Es un mecanismo que monitorea tus servidores y ajusta la cantidad automáticamente. Cuando el tráfico sube, agrega instancias
instancias
Copias independientes de tu aplicación corriendo en un servidor virtual. Cada instancia puede atender requests por su cuenta. Agregar más instancias permite atender más tráfico en paralelo.
. Cuando baja, las elimina. Piensa en un gerente de supermercado omnisciente que sabe exactamente cuándo abrir y cerrar cada caja, pero sin el pánico ni la reacción tardía. Pagas solo por lo que usas, cuando lo usas.
Autoscaling es un principio que existe en prácticamente todas las plataformas de nube. Lo puedes encontrar en AWS, en Google Cloud, en Azure, en Kubernetes. El concepto siempre es el mismo, lo que cambia es cómo lo configuras y qué herramientas usas. En este post vamos a verlo en la práctica usando AWS ECS
, el servicio de contenedores de Amazon, porque es donde lo he implementado y donde puedo mostrarte el flujo completo de principio a fin.
Para que autoscaling funcione, necesitas entender las piezas que lo hacen posible. No es solo "agregar y quitar instancias". Hay un flujo completo detrás, y cada pieza tiene un rol específico. Vamos a recorrerlas una por una.
El load balancer, distribuir para escalar
La primera pieza es el load balancer
load balancer
Componente de red que distribuye el tráfico entrante entre múltiples servidores o instancias de una aplicación, asegurando que ninguna se sobrecargue mientras las demás están ociosas.
. Cuando tu aplicación corre en varias instancias, los usuarios no deberían saber (ni les importa) cuántas hay ni cuál los atiende. Ellos solo ven una URL, un punto de entrada. El load balancer es lo que hace posible esa ilusión. Recibe todas las solicitudes en un solo lugar y las reparte entre las instancias disponibles, sin que el usuario sepa que detrás hay 2, 5 o 50 máquinas trabajando.
Esto es clave para que autoscaling funcione. Si puedes agregar y quitar instancias sin que el usuario lo note, es porque el load balancer absorbe ese cambio. El usuario sigue hablando con la misma URL, y el load balancer se encarga de enviar cada request a una instancia que pueda atenderla. Existen diferentes estrategias para repartir esas requests, como round robin o least connections. Si quieres profundizar en cómo funcionan, tengo un post dedicado a los algoritmos de distribución.
trabaja a nivel HTTP/HTTPS, puede enrutar por path, por host, inspeccionar headers. Es el que usamos con ECS. El Network Load Balancer (NLB)
Network Load Balancer (NLB)
Load balancer de AWS que opera a nivel TCP/UDP (capa 4). Diseñado para conexiones de ultra baja latencia y alto rendimiento, ideal para protocolos que no son HTTP.
trabaja a nivel TCP y es ideal para conexiones de ultra baja latencia o protocolos que no son HTTP. Para nuestra demo usaremos el ALB.
Autoscaling, escalar con la demanda
Ahora que entendemos el load balancer, veamos qué pasa cuando el tráfico sube y las instancias no dan abasto. Con un número fijo de instancias, eventualmente se saturan y empiezan a devolver errores.
Todo funciona
Pocas requests, las instancias procesan sin problema.
request en tránsitoerror 503instancia saludableinstancia caída
Autoscaling resuelve esto. Monitorea una métrica de tu aplicación (uso de CPU, memoria, requests por segundo) y cuando esa métrica cruza un umbral, automáticamente agrega más instancias. Cuando la demanda baja, las elimina. A la acción de agregar se le llama scale-out
scale-out
Agregar más instancias o réplicas de un servicio para distribuir la carga. También conocido como escalado horizontal.
y a la de reducir, scale-in
scale-in
Eliminar instancias o réplicas cuando la demanda baja, reduciendo costos al no mantener capacidad innecesaria.
.
El mismo escenario, pero ahora con autoscaling activo. Cuando las instancias se llenan, aparecen nuevas y el ALB redistribuye la carga automáticamente.
Todo funciona
Pocas requests, 2 instancias.
request en tránsitoinstancia saludableinstancia con carga altanueva instancia (scaling)
Ciclo de autoscaling
AWS ofrece tres tipos de políticas de escalado. La más simple es Target Tracking: le dices "mantén el CPU promedio en 60%" y AWS se encarga de agregar o quitar tareas para lograrlo. Es la que usaremos en la demo. Step Scaling te da más control con reglas manuales: "si CPU pasa de 70% agrega 2, si pasa de 90% agrega 5". Y Scheduled Scaling sirve para patrones predecibles: "todos los lunes a las 9am escala a 10 instancias porque es cuando más tráfico tenemos".
ECS con EC2 vs Fargate
ECS es el orquestador, pero necesita servidores donde correr tus contenedores. La diferencia fundamental entre EC2 y Fargate está en quién gestiona esos servidores. Con EC2, tú provisionas instancias, instalas el agente ECS, defines grupos de autoescalado para la infraestructura y te encargas de parches y actualizaciones. Con Fargate, le dices a AWS cuánta CPU y memoria necesita tu tarea, y AWS se ocupa de todo lo demás: no hay instancias visibles, no hay agentes que instalar, no hay infraestructura que mantener.
ECS con EC2 vs Fargate: quién gestiona qué
Aspecto
ECS con EC2
Fargate
Gestión de infra
Tú gestionas instancias, agente ECS y grupos de autoescalado
AWS gestiona todo, solo defines CPU y memoria
Control
Acceso SSH, tipos de instancia personalizados, GPUs
Sin acceso a la máquina subyacente, limitado a lo que Fargate ofrece
Costo
Más barato a gran escala con instancias reservadas o spot
Pagas por tarea (CPU/memoria exacta), sin costo de instancias ociosas
Autoscaling
Necesitas escalar tareas Y escalar el cluster de EC2
Solo escalas las tareas, la infraestructura se ajusta sola
Cuándo usarlo
Cargas predecibles de gran escala, necesidades especiales de hardware
Equipos pequeños, cargas variables, cuando quieres olvidarte de la infra
Para esta demo usamos Fargate: eliminamos la complejidad de gestionar instancias EC2 y podemos enfocarnos en demostrar el autoscaling de tareas sin distracciones. Con Fargate, cuando ECS decide agregar tareas, no hay que esperar a que una nueva instancia EC2 arranque: AWS aprovisiona la capacidad necesaria de forma transparente.
La app de demo
Construimos una API en Go con dos endpoints. El endpoint /health responde de inmediato con un JSON de estado, es el que el ALB usa para verificar que la tarea está viva. El endpoint /heavy simula carga de CPU: calcula hashes SHA-256 en un ciclo durante aproximadamente 500ms. Este es el endpoint que vamos a bombardear con Vegeta para que el CPU suba y ECS active el autoscaling.
go
package main
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
http.HandleFunc("/heavy", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Simular carga CPU durante ~500ms
for time.Since(start) < 500*time.Millisecond {
data := fmt.Sprintf("work-%d", time.Now().UnixNano())
sha256.Sum256([]byte(data))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"duration": time.Since(start).String(),
})
})
fmt.Println("Server running on :8080")
http.ListenAndServe(":8080", nil)
}
Para empaquetar la app usamos un Dockerfile multistage: la primera etapa compila el binario con la imagen de Go, y la segunda etapa copia solo ese binario a una imagen Alpine mínima. El resultado es una imagen final de aproximadamente 15MB, lo que hace que las nuevas tareas arranquen rápido cuando ECS necesita escalar.
dockerfile
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o server main.go
FROM alpine:3.19
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
Desplegando en AWS
Paso 1: Subir la imagen a Docker Hub
ECS necesita descargar nuestra imagen de algún registro de contenedores. Podríamos usar ECR (el registro privado de AWS), pero eso agrega pasos de autenticación y configuración que no aportan nada al tema central del post. Docker Hub es más directo: hacemos build, tag y push. ECS soporta imágenes públicas de Docker Hub sin configuración adicional.
La task definition le indica a ECS cómo ejecutar nuestro contenedor: qué imagen usar, cuánta CPU y memoria asignar, qué puerto exponer y cómo configurar los logs en CloudWatch. Guardamos este JSON como task-definition.json y luego lo registramos con el CLI.
Antes de crear el servicio ECS, necesitamos un Application Load Balancer y un target group. El target group es donde ECS registrará las IPs de cada tarea que levante. El comando siguiente crea el servicio ECS conectado a ese ALB, comenzando con una sola tarea.
Una vez que el servicio levanta, el ALB muestra la tarea registrada como target healthy. Una sola instancia, lista para recibir tráfico.
Paso 5: Configurar autoscaling
Con el servicio corriendo, configuramos la política de escalado automático. Primero registramos el servicio como un target escalable con un mínimo de 1 tarea y un máximo de 10. Luego creamos una política de tipo Target Tracking que mantiene el CPU promedio en 60%: cuando supere ese umbral ECS agregará tareas, y cuando baje las reducirá gradualmente respetando el cooldown de 120 segundos para el scale-in.
Con toda la infraestructura lista, es hora de verificar que el autoscaling funciona de verdad. Para eso usamos Vegeta, una herramienta de load testing escrita en Go que permite generar carga HTTP de forma controlada: defines la tasa de requests por segundo y la duración del ataque, y Vegeta se encarga del resto. Es liviana, precisa y perfecta para este tipo de demos.
El siguiente comando envía 50 requests por segundo al endpoint /heavy durante 5 minutos. Cada request hará que la tarea queme CPU durante ~500ms calculando hashes SHA-256, lo que debería subir la utilización de CPU por encima del umbral de 60% y disparar el autoscaling.
En la gráfica del ALB se puede ver el momento exacto en que Vegeta empieza a atacar. Los requests suben de 0 a más de 2,700 en cuestión de minutos. La curva sube, se mantiene, y eventualmente baja cuando el ataque termina.
Mientras eso ocurre, CloudWatch detecta que el CPU del servicio supera el 60%. La alarma se activa, la scaling policy responde, y ECS levanta una segunda tarea. El ALB la registra automáticamente y empieza a distribuirle tráfico. Ahora hay 2 targets healthy, compartiendo la carga.
Cuando Vegeta deja de atacar, el CPU cae. CloudWatch lo detecta, la alarma se desactiva, y después del cooldown de 120 segundos ECS decide que ya no necesita la segunda tarea. Empieza el proceso de scale-in. En la consola se puede ver cómo el segundo target pasa a estado "Draining", que significa que está terminando las conexiones activas antes de ser eliminado. Es un shutdown limpio, sin cortar requests a la mitad.
Eso es autoscaling en acción. Una sola tarea cuando el tráfico es bajo, dos (o más) cuando sube, y de vuelta a una cuando la demanda pasa. Sin intervención manual, sin gerentes en pánico.
Arquitectura completa de la demo
Cosas que aprendí a la mala
Autoscaling funciona. Pero hay detalles que no aparecen en la documentación y que solo descubres cuando algo se rompe en producción a las 3 de la mañana.
No escales en pánico
Cuando ECS agrega tareas, esas tareas necesitan tiempo para iniciar, registrarse en el ALB y empezar a absorber carga real. Si el cooldown es muy bajo, el sistema ve que el CPU sigue alto (porque las tareas nuevas aún no contribuyen), agrega más, y terminas con 15 instancias cuando necesitabas 3. Eso se llama thrashing
thrashing
Oscilación repetida entre scale-out y scale-in. El sistema escala hacia arriba, no espera lo suficiente, escala hacia abajo, el CPU vuelve a subir, y el ciclo se repite sin estabilizarse nunca.
y te genera una factura sorpresa. Configura ScaleOutCooldown y ScaleInCooldown con valores conservadores. 60 segundos para scale-out y 120 para scale-in es un buen punto de partida.
El health check te puede sabotear
Si tu aplicación tarda 10 segundos en arrancar y el health check empieza a evaluar a los 5, la tarea va a fallar los checks, el ALB la marca como unhealthy, ECS la mata y levanta otra. Esa otra también falla. El servicio entra en un loop infinito de tareas muriendo y resucitando. Configura el healthCheckGracePeriodSeconds del servicio ECS para darle tiempo a tu aplicación de estar lista antes de que el ALB empiece a evaluarla.
CPU no siempre es la mejor métrica
Para la demo usamos CPU porque es simple y visual. Pero en APIs de producción, escalar por requests por segundo o latencia p99 refleja mucho mejor la experiencia del usuario. Un servicio puede tener CPU bajo y aún así estar respondiendo lento porque está esperando respuestas de una base de datos. CloudWatch te permite crear métricas custom y usarlas como trigger de autoscaling.
Pon un límite máximo. Siempre.
Fargate cobra por vCPU-hora y GB-hora. Cada tarea que se levanta tiene un costo real. Si no pones un max-capacity razonable, un pico de tráfico inesperado (o un bot que te esté pegando) puede escalar tu servicio a 50 instancias y generar una factura de miles de dólares en un fin de semana. Pon un máximo, monitorea las alarmas de billing, y duerme tranquilo.
Logs centralizados no son opcionales
Cuando tienes 10 tareas corriendo en paralelo y algo falla, necesitas saber en cuál. Configura todas las tareas para enviar logs a CloudWatch con el ID de la tarea en el prefijo del stream. Sin eso, debuggear en producción es como buscar una aguja en un pajar con los ojos vendados.
Conclusión
En este post vimos cómo funciona el autoscaling en AWS ECS: el rol del load balancer para distribuir tráfico entre tareas, la diferencia entre EC2 y Fargate como estrategias de capacidad, y cómo Application Auto Scaling usa métricas de CloudWatch para tomar decisiones de escalado. También vimos el flujo completo en acción: desde el despliegue de la API en Go hasta la generación de carga con Vegeta y la observación de cómo ECS responde agregando tareas automáticamente.
En un próximo post vamos a explorar el mismo problema pero en Kubernetes: cómo funciona el Horizontal Pod Autoscaler, qué papel juega el cluster autoscaler para gestionar los nodos, y cómo se compara esa experiencia con lo que acabamos de ver en ECS. El contraste entre los dos enfoques ayuda a entender mejor los trade-offs de cada plataforma.