Entendiendo el Autoscaling usando AWS ECS

Jorge SaavedraJorge Saavedra
·9 de abril, 2026·12 min de lectura
awsecsfargateautoscalinggodevops
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.
Clientes
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.
Clientes
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í.
Clientes
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
AWS ECS

Amazon Elastic Container Service. Servicio de orquestación de contenedores que permite ejecutar y escalar aplicaciones en contenedores Docker sin gestionar la infraestructura subyacente.

, 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.

Application Load Balancer distribuyendo requests

Usuariosuna sola URLALBpunto de entradaúnicoInstancia 1Instancia 2Instancia 3
AWS ofrece dos tipos principales de load balancer. El Application Load Balancer (ALB)
Application Load Balancer (ALB)

Load balancer de AWS que opera a nivel HTTP/HTTPS (capa 7). Puede enrutar tráfico basándose en el path de la URL, el host, headers y otros atributos de la request.

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.
ALBRequests
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.
ALBRequests
Todo funciona
Pocas requests, 2 instancias.
request en tránsitoinstancia saludableinstancia con carga altanueva instancia (scaling)

Ciclo de autoscaling

CloudWatchCPU > 60%Alarmase activaScaling Policy+2 tareasECS Servicenuevas tareasse registran en ALBScale-outScale-in, métrica baja, tareas se eliminan
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é

ECS con EC2Instancias EC2servidores, SO, parchesECS Agentinstalado en cada instanciaTareastus contenedoresTú gestionas todoFargateInfraestructura AWSservidores, SO, agenteparches, escalado de infrainvisible para tiTareastus contenedoresAWS gestiona la infraTú gestionasAWS gestiona
AspectoECS con EC2Fargate
Gestión de infraTú gestionas instancias, agente ECS y grupos de autoescaladoAWS gestiona todo, solo defines CPU y memoria
ControlAcceso SSH, tipos de instancia personalizados, GPUsSin acceso a la máquina subyacente, limitado a lo que Fargate ofrece
CostoMás barato a gran escala con instancias reservadas o spotPagas por tarea (CPU/memoria exacta), sin costo de instancias ociosas
AutoscalingNecesitas escalar tareas Y escalar el cluster de EC2Solo escalas las tareas, la infraestructura se ajusta sola
Cuándo usarloCargas predecibles de gran escala, necesidades especiales de hardwareEquipos 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.
bash
docker build -t autoscaling-demo .
docker tag autoscaling-demo:latest <tu-usuario>/autoscaling-demo:latest
docker push <tu-usuario>/autoscaling-demo:latest

Paso 2: Cluster ECS

Creamos un cluster de ECS con soporte para Fargate. El cluster es el agrupador lógico donde vivirán nuestras tareas y servicios.
bash
aws ecs create-cluster --cluster-name autoscaling-demo --capacity-providers FARGATE

Paso 3: Task Definition

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.
json
{
  "family": "autoscaling-demo",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "<tu-usuario>/autoscaling-demo:latest",
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/autoscaling-demo",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "api"
        }
      }
    }
  ]
}
Registramos la task definition:
bash
aws ecs register-task-definition --cli-input-json file://task-definition.json

Paso 4: Servicio con ALB

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.
bash
# Crear servicio ECS con ALB (simplificado)
aws ecs create-service \
  --cluster autoscaling-demo \
  --service-name api-service \
  --task-definition autoscaling-demo \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx],assignPublicIp=ENABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=api,containerPort=8080"
Una vez que el servicio levanta, el ALB muestra la tarea registrada como target healthy. Una sola instancia, lista para recibir tráfico.
ALB con una sola tarea healthy

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.
bash
# Registrar target escalable
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/autoscaling-demo/api-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 1 \
  --max-capacity 10

# Crear política de target tracking
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/autoscaling-demo/api-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-target-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 60.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleOutCooldown": 60,
    "ScaleInCooldown": 120
  }'

Probando el autoscaling con Vegeta

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.
bash
echo "GET https://tu-alb-url.amazonaws.com/heavy" | vegeta attack -duration=5m -rate=50/s | vegeta report
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.
Gráfica de requests del ALB durante el ataque con Vegeta
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.
ALB mostrando 2 targets healthy después del scale-out
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.
ALB mostrando scale-in, un target draining
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

Vegeta50 req/sALBdistribuye requestsECS Fargate1..N tareasapi:8080CloudWatchCPU > 60% → alarmaScaling PolicyTarget Trackingscale

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.

Posts que podrian interesarte