Toda la evidencia
AWSFinOpsTerraformECS

Reducir costes en ECS: de Fargate a EC2 con despliegues de hosts inmutables y sin downtime

Resumen

Migrar los servicios de ECS en producción de Fargate a EC2 y capacidad spot redujo alrededor de un 44 % el coste de cómputo por unidad de carga de trabajo, y los equipos de aplicación no tuvieron que cambiar nada.

Fargate cobra un sobreprecio para que nadie tenga que gestionar hosts. Conservamos la mayor parte de esa comodidad sobre capacidad más barata: los hosts son inmutables y se sustituyen automáticamente sin downtime, las interrupciones de spot se drenan de forma ordenada y cada tarea mantiene su propia interfaz de red y su propio security group.

El ahorro es menor que la diferencia de precio bruta entre Fargate y EC2, y este caso de éxito explica por qué.

El reto

Los servicios de ECS en producción se ejecutaban en Fargate, y la factura de cómputo crecía con cada nuevo servicio, hasta unos 1.700 $ al mes.

El precio de Fargate incluye la gestión de los hosts: no hay AMIs que parchear, ni instancias que dimensionar, ni capacidad que planificar. En Fráncfort (eu-central-1) eso cuesta 0,04656 $ por vCPU-hora y 0,00511 $ por GB-hora. EC2, y spot en particular, es mucho más barato para el mismo cómputo, pero solo si asumes el trabajo que hacía Fargate.

Así que la verdadera pregunta era cuánto de ese trabajo podíamos automatizar para que el ahorro no se fuera en operaciones.

Restricciones

  1. Paridad con Fargate. Los equipos de aplicación no deberían notar nada: el mismo modelo de red, el mismo flujo de despliegue, ningún runbook nuevo.
  2. Nada de copos de nieve. Todo vive en Terraform sobre el stack existente de terraform-aws-modules (VPC, ALB, ECS, Auto Scaling). Ningún host se configura nunca a mano.
  3. Construir de forma incremental. Empezamos con una VPC mínima, un ALB, un clúster sobre EC2 y un servicio nginx, y después añadimos las funciones de producción una a una, validando cada una antes de pasar a la siguiente.

Hosts inmutables: sustituir, nunca parchear

En Fargate, AWS parchea los hosts. En EC2 ese trabajo pasó a ser nuestro, así que lo convertimos en una tarea de sustitución en lugar de mantenimiento: ningún host se parchea nunca in situ.

Una nueva AMI optimizada para ECS supone una nueva versión del launch template, lo que dispara un instance refresh de Auto Scaling que sustituye todos los hosts. Los contenedores se mueven sin downtime porque cada host se drena antes de terminarse:

  1. Terraform publica una versión del launch template con la nueva AMI, y empieza el instance refresh.
  2. Auto Scaling elige los hosts antiguos que va a sustituir, en lotes limitados por su porcentaje mínimo de instancias sanas.
  3. Cada host elegido pasa por el drenaje gestionado de ECS. ECS reprograma sus tareas en otros hosts respetando el porcentaje mínimo de tareas sanas de cada servicio, y el ALB da de baja los targets antiguos.
  4. El host solo se termina cuando ya no ejecuta ninguna tarea, y los hosts nuevos con la nueva AMI ocupan su lugar.

La trampa: la protección frente a scale-in. La protección de terminación gestionada marca como protegido cada host que ejecuta tareas, lo que impide que el scale-in mate tareas. El instance refresh tiene una preferencia específica para las instancias protegidas, y cada configuración equivocada falla de una forma distinta. Por defecto, el refresh se salta los hosts protegidos y puede terminar con AMIs antiguas todavía sirviendo tráfico. Wait se queda bloqueado, porque ECS solo retira la protección de un host vacío. Sustituir hosts protegidos sin drenaje gestionado los termina con tareas todavía en ejecución.

Configuramos ScaleInProtectedInstances = Refresh y nos apoyamos en el drenaje gestionado: los hosts protegidos se siguen sustituyendo, pero solo después de que ECS haya movido sus tareas.

Este bucle es lo que sustituye al parcheado que antes hacía Fargate por nosotros, y es el núcleo de la paridad con Fargate.

Base on-demand más spot

El clúster ejecuta un host on-demand estable y pone todo lo demás en spot. Había dos formas de montarlo, y solo una garantiza dónde acaban las tareas críticas.

Opción A: un ASG con política de instancias mixtasOpción B: dos capacity providers
EstructuraUn Auto Scaling group que mezcla on-demand y spotUn grupo on-demand y un grupo spot, cada uno con su propio capacity provider
La base on-demand cuentaInstanciasTareas (base en la estrategia)
ECS distingue on-demand de spotNoSí
Tareas críticas garantizadas en on-demandNoSí

Elegimos la opción B. Con un único grupo mixto, ECS ve un único conjunto de hosts, así que una tarea crítica puede acabar en spot. Con dos capacity providers, la estrategia de capacity providers de cada servicio coloca sus primeras base tareas en on-demand y reparte el resto según weight.

El detalle que confunde a mucha gente: base cuenta tareas, no instancias. Una base de 2 significa dos tareas en capacidad on-demand, que pueden compartir un mismo host.

Las tareas críticas van primero a on-demand; el resto, a spot. La estrategia de capacity providers de un servicio de ECS envía sus tareas base al capacity provider on-demand y todo lo demás, según su peso, al capacity provider spot. Cada provider se apoya en su propio Auto Scaling group: un host on-demand estable, y spot repartido entre varios tipos de instancia y zonas de disponibilidad.

La estrategia de cada servicio llena on-demand hasta su base y después envía las tareas restantes a spot.

Mantener la sensación de Fargate

Red. Todos los servicios siguen en modo awsvpc, así que cada tarea tiene su propia ENI y su propio security group, exactamente como en Fargate. El precio en EC2 es la densidad: cada tarea usa una ENI, y cada tipo de instancia limita cuántas puede asociar. El ENI trunking eleva ese límite, pero solo en los tipos de instancia compatibles, lo que reduce el abanico de spot. Usamos hosts xlarge (4 vCPU, 16 GB) con trunking activado, y cada uno lleva entre 6 y 8 tareas, así que el límite lo marca la CPU, no las ENIs.

Interrupciones de spot. El drenaje gestionado en los capacity providers, junto con ECS_ENABLE_SPOT_INSTANCE_DRAINING=true en la configuración del agente, convierte el aviso de interrupción de spot de dos minutos en un drenaje ordenado. ECS reprograma las tareas y el ALB las da de baja antes de que el host desaparezca.

Margen. Un target_capacity del managed scaling por debajo del 100 % mantiene espacio libre en el clúster, de modo que las tareas nuevas arrancan al momento en lugar de esperar en PROVISIONING a que haya un host. Lo fijamos en el 90 %, cambiando capacidad ociosa por latencia de escalado.

Resiliencia de spot. El grupo spot se reparte entre cuatro tipos de instancia (m5.xlarge, m5d.xlarge, m6i.xlarge, m6id.xlarge) y tres zonas de disponibilidad, así que una recuperación de capacidad en un pool nunca tumba un servicio entero. Usa la estrategia de asignación price-capacity-optimized, y el capacity rebalancing sustituye los hosts en riesgo antes de que AWS los recupere.

Medir el escalado

Un nuevo host spot tarda unos 170 segundos, algo menos de tres minutos, desde que se solicita la tarea hasta que hay una tarea sana detrás del ALB. «¿Cuánto tarda un host en frío?» era la mayor preocupación al dejar Fargate, así que lo medimos en lugar de discutirlo.

La prueba fue una pequeña aplicación Next.js con renderizado en servidor, escalada hasta que ECS tuvo que pedir un nuevo host spot.

FaseTiempo (s)
De la solicitud de la tarea al arranque del host~75
Del arranque del host al registro en el clúster~50
Del registro a la tarea en RUNNING~25
De RUNNING a sana en el target group del ALB~20
Total~170

Esa cifra determinó el margen: cuanto más lento es un host en frío, más capacidad libre tiene que reservar target_capacity.

Resultados

Por unidad de carga de trabajo, el coste de cómputo bajó de 0,05678 $ a unos 0,032 $ por tarea-hora, alrededor de un 44 % menos: unos 1.720 $ al mes en Fargate frente a 970 $ en EC2 y spot. La referencia es una tarea de 1 vCPU y 2 GB con las tarifas de Fargate en Fráncfort (0,04656 $ + 2 × 0,00511 $).

MétricaFargateEC2 + spot
Coste mensual de cómputo, misma carga~1.720 $~970 $
Coste por tarea-hora, 1 vCPU / 2 GB0,05678 $~0,032 $
Reserva de CPU / memoria del clústerSe factura por tarea~74 % / ~46 %
Despliegue completo de AMI, 14 hostsLo gestiona AWS~35 min, 0 peticiones fallidas en el ALB
Interrupciones de spot al mesNingunaDe 1 a 3, ninguna visible para los usuarios

Para que la comparación sea honesta, la cifra de EC2 es el coste de todo el clúster, EBS incluido, dividido entre las vCPU-hora y GB-hora que las tareas reservaron realmente. El margen ocioso, el desperdicio del bin-packing y la sobrecarga de los hosts están incluidos en esa cifra.

A qué renunciamos

El ahorro es real, pero menor que la diferencia de precio bruta, por cuatro motivos:

  • Desperdicio del bin-packing. Ahora pagamos capacidad de host que ninguna tarea está usando, algo que Fargate nunca cobraba.
  • Sobrecarga de los hosts. El agente de ECS y los daemons que haya se quedan con una parte de cada instancia.
  • Responsabilidad. El ritmo de actualización de las AMIs y el flujo de sustitución son ahora trabajo nuestro, no de AWS.
  • Interrupciones. Que AWS recupere capacidad spot es un evento normal, y todos los servicios tienen que tolerarlo.

Preferimos decirlo de entrada antes que dejar que un porcentaje en un titular sugiera que nada de esto tiene coste.

Qué haríamos de otra manera

  • Instancias Graviton, para un precio por vCPU más bajo tanto en on-demand como en spot.
  • Un Compute Savings Plan que cubra la base on-demand, que es lo bastante estable como para comprometerse con ella.
  • Un flujo automatizado de AMIs que lance un instance refresh en cuanto AWS publique una nueva AMI optimizada para ECS, para que el ritmo de parcheado ya no dependa de que alguien se dé cuenta.