DevOps desde Cero: Deployeando Tu API a AWS ECS con Fargate
Apoya este blog
Si te resulta util este contenido, considera apoyar el blog.
Introduccion
Bienvenido al articulo ocho de la serie DevOps desde Cero. En el articulo anterior aprendimos como provisionar infraestructura AWS con Terraform. Ahora es momento de poner ese conocimiento en practica y deployear nuestra API TypeScript de tareas (la que construimos en el articulo dos) a un entorno cloud real usando AWS ECS con Fargate.
ECS (Elastic Container Service) es la plataforma de orquestacion de containers propia de AWS. Te permite correr containers Docker sin tener que gestionar la infraestructura subyacente vos mismo. Cuando lo combinas con Fargate, ni siquiera tenes que pensar en instancias EC2. Simplemente definis lo que necesita tu container y AWS se encarga del resto. Es un excelente punto de partida antes de meternos con Kubernetes mas adelante en la serie.
En este articulo vamos a cubrir los conceptos centrales de ECS, pushear nuestra imagen Docker a un registry, escribir codigo Terraform para provisionar todo (cluster, servicio, load balancer, auto-scaling), deployear la API y verificar que esta corriendo. Al final vas a tener un deployment listo para produccion que escala automaticamente segun la demanda.
Vamos a meternos de lleno.
Que es ECS?
Amazon Elastic Container Service (ECS) es un servicio de orquestacion de containers completamente gestionado. En lugar de instalar y gestionar tu propio orquestador (como Kubernetes), le das tus definiciones de container a ECS y el se encarga del scheduling, scaling y networking por vos.
Hay cuatro conceptos clave que necesitas entender:
- Cluster: Un agrupamiento logico de recursos donde corren tus containers. Pensalo como el limite que mantiene todo junto. Un cluster puede contener multiples servicios.
- Task Definition: Un plano de tu container. Especifica que imagen Docker usar, cuanta CPU y memoria asignar, que puertos exponer, que variables de entorno setear, y a donde enviar los logs. Esta versionado, asi que podes volver a una definicion anterior si es necesario.
- Task: Una instancia en ejecucion de un task definition. Si el task definition es la receta, el task es el plato que se esta sirviendo. Cada task corre uno o mas containers.
- Service: Un constructo de larga duracion que asegura que una cantidad especificada de tasks esten siempre corriendo. Si un task se cae, el servicio automaticamente inicia uno nuevo. Los servicios tambien manejan rolling deployments cuando actualizas tu task definition.
Asi es como encajan estas piezas:
ECS Cluster
└── Service (mantiene la cantidad deseada de tasks)
├── Task 1 (container corriendo basado en task definition v3)
├── Task 2 (container corriendo basado en task definition v3)
└── Task 3 (container corriendo basado en task definition v3)
Tipos de lanzamiento en ECS: Fargate vs EC2
Cuando creas un servicio ECS, elegis un tipo de lanzamiento que determina donde corren realmente tus containers:
- Tipo de lanzamiento EC2: Vos gestionas una flota de instancias EC2. ECS programa los containers en esas instancias. Vos sos responsable de los parches, el scaling y el mantenimiento de las instancias. Mas control, mas trabajo.
- Tipo de lanzamiento Fargate: AWS gestiona el computo. Solo especificas CPU y memoria para cada task, y Fargate provisiona la cantidad correcta de computo detras de escena. Sin servidores que gestionar, sin planificacion de capacidad, sin parches de SO.
Para este articulo usamos Fargate porque elimina una capa entera de complejidad. Pagas un poco mas comparado con EC2, pero te ahorras un monton de esfuerzo operativo. Para la mayoria de los equipos que estan empezando, Fargate es la eleccion correcta.
ECS vs EKS: una comparacion breve
Te podrias preguntar por que no vamos directo a Kubernetes. AWS ofrece EKS (Elastic Kubernetes Service) para eso. Aca va la comparacion rapida:
- ECS es mas simple de configurar, esta integrado estrechamente con servicios AWS, y no tiene costo de control plane con Fargate. Si tus workloads son solo AWS, ECS te pone en marcha mas rapido.
- EKS te da la API completa de Kubernetes, portabilidad entre nubes, y acceso al ecosistema masivo de Kubernetes. Es mas complejo pero mas flexible.
Vamos a cubrir EKS en profundidad mas adelante en la serie. Por ahora, ECS con Fargate es el escalon perfecto porque te ensenia conceptos de orquestacion de containers sin la curva de aprendizaje de Kubernetes.
Pusheando tu imagen Docker a ECR
Antes de que ECS pueda correr tu container, la imagen necesita estar almacenada en un container registry al que ECS pueda acceder. AWS provee ECR (Elastic Container Registry) para esto. Tambien podrias usar GitHub Container Registry (GHCR) o Docker Hub, pero ECR se integra transparentemente con ECS, asi que es la opcion mas simple.
Primero, crea un repositorio ECR usando el AWS CLI:
aws ecr create-repository \
--repository-name task-api \
--region us-east-1 \
--image-scanning-configuration scanOnPush=true
El flag scanOnPush=true habilita escaneo automatico de vulnerabilidades en cada push. Es una
funcionalidad gratuita y no hay razon para no usarla.
Ahora autenticate Docker con ECR, construi la imagen, etiquetala y pusheala:
# Obtener el token de login y pasarselo a docker login
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Construir la imagen (usando el Dockerfile del articulo 2)
docker build -t task-api .
# Etiquetar para ECR
docker tag task-api:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest
# Pushear a ECR
docker push \
123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest
Reemplaza 123456789012 con tu ID de cuenta AWS real. Lo podes encontrar ejecutando
aws sts get-caller-identity --query Account --output text.
Estructura del proyecto Terraform
Vamos a provisionar todo con Terraform, construyendo sobre las bases del articulo siete. Esta es la estructura del proyecto con la que vamos a terminar:
infra/
├── main.tf # Configuracion del provider y backend
├── variables.tf # Variables de entrada
├── outputs.tf # Valores de salida
├── vpc.tf # VPC, subnets, internet gateway
├── ecr.tf # Repositorio ECR
├── ecs.tf # Cluster ECS, task definition, servicio
├── alb.tf # Application Load Balancer
├── autoscaling.tf # Politicas de auto-scaling
├── iam.tf # Roles y politicas IAM
└── security_groups.tf # Security groups
Empecemos con la configuracion del provider y las variables.
Provider y variables
El archivo main.tf configura el provider de AWS y el backend de Terraform:
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "ecs/task-api/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = var.aws_region
}
Ahora defini las variables que vamos a usar a lo largo de la configuracion:
# variables.tf
variable "aws_region" {
description = "Region de AWS para deployear"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Nombre del proyecto, usado para nombrar recursos"
type = string
default = "task-api"
}
variable "environment" {
description = "Entorno de deployment"
type = string
default = "production"
}
variable "container_port" {
description = "Puerto en el que escucha el container"
type = number
default = 3000
}
variable "container_cpu" {
description = "Unidades de CPU para el container (1024 = 1 vCPU)"
type = number
default = 256
}
variable "container_memory" {
description = "Memoria en MiB para el container"
type = number
default = 512
}
variable "desired_count" {
description = "Numero de tasks a correr"
type = number
default = 2
}
variable "container_image" {
description = "URI de la imagen Docker para el container"
type = string
}
Networking: VPC y subnets
Nuestro servicio ECS necesita una VPC con subnets publicas y privadas. El ALB va a estar en las subnets publicas, y los tasks de Fargate van a correr en las subnets privadas:
# vpc.tf
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
}
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.project_name}-nat-eip"
}
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.project_name}-nat"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = {
Name = "${var.project_name}-private-rt"
}
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
Algunas cosas para notar aca. Las subnets publicas tienen una ruta al internet gateway, que es donde va a estar nuestro ALB. Las subnets privadas enrutan a traves de un NAT gateway, que le permite a nuestros tasks de Fargate pullear imagenes de ECR y enviar logs a CloudWatch sin estar directamente expuestos a internet. Este es un patron estandar para workloads de produccion.
Security groups
Necesitamos dos security groups: uno para el ALB (permite trafico HTTP entrante desde internet) y uno para los tasks de ECS (permite trafico solo desde el ALB):
# security_groups.tf
resource "aws_security_group" "alb" {
name = "${var.project_name}-alb-sg"
description = "Security group para el Application Load Balancer"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP desde cualquier lugar"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-alb-sg"
}
}
resource "aws_security_group" "ecs_tasks" {
name = "${var.project_name}-ecs-tasks-sg"
description = "Security group para los tasks de ECS"
vpc_id = aws_vpc.main.id
ingress {
description = "Permitir trafico desde el ALB"
from_port = var.container_port
to_port = var.container_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-ecs-tasks-sg"
}
}
Esto es el principio de minimo privilegio aplicado al networking. Los tasks de ECS solo aceptan trafico del ALB, no del internet publico directamente. El ALB es el unico punto de entrada.
Roles IAM para ECS
Los tasks de ECS necesitan dos roles IAM: un rol de ejecucion (usado por ECS mismo para pullear imagenes y escribir logs) y un rol de tarea (usado por el codigo de tu aplicacion para acceder a servicios AWS):
# iam.tf
resource "aws_iam_role" "ecs_execution_role" {
name = "${var.project_name}-ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "ecs_task_role" {
name = "${var.project_name}-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
El rol de ejecucion recibe la politica gestionada AmazonECSTaskExecutionRolePolicy, que otorga
permisos para pullear imagenes de ECR y escribir logs en CloudWatch. El rol de tarea empieza vacio.
A medida que tu aplicacion crezca y necesite acceso a otros servicios AWS (S3, DynamoDB, SQS, etc.),
le irias adjuntando politicas a este rol. Mantenelos separados para que tengas limites claros entre
lo que necesita ECS y lo que necesita tu app.
El repositorio ECR en Terraform
En lugar de crear el repositorio ECR manualmente con el CLI, vamos a gestionarlo con Terraform para que todo este en codigo:
# ecr.tf
resource "aws_ecr_repository" "app" {
name = var.project_name
image_tag_mutability = "MUTABLE"
force_delete = true
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = var.project_name
}
}
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Mantener solo las ultimas 10 imagenes"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
}
]
})
}
La politica de ciclo de vida es importante. Sin ella, tu repositorio ECR va a acumular imagenes viejas indefinidamente, y vas a pagar por el almacenamiento. Esta politica mantiene solo las ultimas 10 imagenes y expira el resto automaticamente.
Cluster ECS, task definition y servicio
Ahora el plato fuerte. Vamos a crear el cluster ECS, definir nuestro task y crear un servicio que lo mantenga corriendo:
# ecs.tf
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${var.project_name}"
retention_in_days = 30
tags = {
Name = var.project_name
}
}
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${var.project_name}-cluster"
}
}
resource "aws_ecs_task_definition" "app" {
family = var.project_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.container_cpu
memory = var.container_memory
execution_role_arn = aws_iam_role.ecs_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = var.project_name
image = var.container_image
essential = true
portMappings = [
{
containerPort = var.container_port
protocol = "tcp"
}
]
environment = [
{
name = "NODE_ENV"
value = "production"
},
{
name = "PORT"
value = tostring(var.container_port)
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
}
resource "aws_ecs_service" "app" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.desired_count
launch_type = "FARGATE"
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
health_check_grace_period_seconds = 60
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = var.project_name
container_port = var.container_port
}
deployment_circuit_breaker {
enable = true
rollback = true
}
depends_on = [aws_lb_listener.http]
}
Estan pasando muchas cosas aca, asi que vamos a desglosarlo pieza por pieza.
El log group de CloudWatch es donde se van a enviar todos los logs del container. Setear
retention_in_days en 30 evita que los logs se acumulen para siempre y te inflen la factura.
El cluster es directo. Habilitamos Container Insights para mejores metricas de monitoreo.
El task definition es la parte mas detallada:
network_mode = "awsvpc"le da a cada task su propia interfaz de red elastica. Es requerido para Fargate.cpuymemorydefinen el tamano de Fargate. 256 unidades de CPU (0.25 vCPU) y 512 MiB es la configuracion mas chica y funciona bien para una API liviana.- El bloque
container_definitionsdefine el container: imagen, mapeos de puertos, variables de entorno, configuracion de logs y health check.- El health check ejecuta
curlcontra el endpoint/healthcada 30 segundos. Si tres chequeos consecutivos fallan, ECS marca el task como unhealthy y lo reemplaza.
El servicio conecta todo:
desired_count = 2significa que ECS siempre va a intentar mantener dos tasks corriendo.deployment_minimum_healthy_percent = 50significa que durante un deployment, al menos un task (50% de 2) debe permanecer sano. Esto permite rolling updates sin downtime.deployment_maximum_percent = 200significa que ECS puede temporalmente correr hasta cuatro tasks durante un deployment (los viejos mas los nuevos).- El
deployment_circuit_breakerautomaticamente hace rollback de un deployment si los nuevos tasks no logran estabilizarse. Esto evita que una imagen rota tire abajo tu servicio.
Application Load Balancer
El ALB se pone enfrente de tu servicio ECS, distribuye trafico entre los tasks, y provee un endpoint estable para los clientes. Tambien maneja health checks para asegurarse de que el trafico solo vaya a tasks saludables:
# alb.tf
resource "aws_lb" "app" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
tags = {
Name = "${var.project_name}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
interval = 30
path = "/health"
protocol = "HTTP"
matcher = "200"
}
deregistration_delay = 30
tags = {
Name = "${var.project_name}-tg"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.app.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
Algunos detalles importantes:
target_type = "ip"es requerido para Fargate. Con el tipo de lanzamiento EC2 usariasinstance, pero los tasks de Fargate obtienen sus propias direcciones IP.- El health check golpea
/healthy espera una respuesta200. Si un task falla tres chequeos consecutivos, el ALB deja de enviarle trafico y ECS lo reemplaza.deregistration_delay = 30le da a las requests en curso 30 segundos para completarse antes de que un task sea removido del target group durante deployments. El default es 300 segundos, que es demasiado para la mayoria de las APIs.
En produccion agregarias soporte HTTPS con un certificado ACM y un listener en el puerto 443. Lo mantenemos simple con HTTP por ahora, pero no expongas APIs de produccion por HTTP plano.
Auto-scaling
Correr un numero fijo de tasks funciona, pero desperdicia plata en periodos de poco trafico y arriesga sobrecarga durante picos. ECS se integra con Application Auto Scaling para ajustar la cantidad de tasks basandose en metricas:
# autoscaling.tf
resource "aws_appautoscaling_target" "ecs" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.project_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
name = "${var.project_name}-memory-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = 80.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
Esto es lo que hace:
- Minimo 2, maximo 10 tasks. Siempre tenes al menos dos tasks corriendo para disponibilidad, y caps en diez para controlar costos.
- Target de CPU: 70%. Si el promedio de CPU entre todos los tasks supera 70%, ECS agrega mas tasks. Si baja bastante por debajo de 70%, ECS remueve tasks (hasta el minimo de 2).
- Target de memoria: 80%. Misma idea, pero para utilizacion de memoria.
- Cooldown de scale-out: 60 segundos. Despues de agregar tasks, esperar al menos 60 segundos antes de considerar agregar mas. Esto previene el thrashing.
- Cooldown de scale-in: 300 segundos. Despues de remover tasks, esperar 5 minutos antes de considerar remover mas. Esto es deliberadamente mas lento para evitar scale-down prematuro.
Target tracking es la estrategia de auto-scaling mas simple y funciona bien para la mayoria de los workloads. Le decis a AWS “mantene la CPU alrededor de 70%” y el se da cuenta de cuantos tasks correr. Si tus necesidades de scaling son mas complejas, podes usar step scaling policies o scheduled scaling, pero target tracking es un buen default.
Outputs
Finalmente, defini outputs para que puedas encontrar facilmente la URL del ALB y otra informacion util despues de deployear:
# outputs.tf
output "alb_dns_name" {
description = "Nombre DNS del Application Load Balancer"
value = aws_lb.app.dns_name
}
output "ecr_repository_url" {
description = "URL del repositorio ECR"
value = aws_ecr_repository.app.repository_url
}
output "ecs_cluster_name" {
description = "Nombre del cluster ECS"
value = aws_ecs_cluster.main.name
}
output "ecs_service_name" {
description = "Nombre del servicio ECS"
value = aws_ecs_service.app.name
}
output "cloudwatch_log_group" {
description = "Log group de CloudWatch para los tasks de ECS"
value = aws_cloudwatch_log_group.app.name
}
Deployeando con Terraform
Con toda la configuracion lista, deployear es cuestion de ejecutar el flujo estandar de Terraform:
cd infra
# Inicializar Terraform (descargar providers, configurar backend)
terraform init
# Revisar el plan de ejecucion
terraform plan -var="container_image=123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest"
# Aplicar los cambios
terraform apply -var="container_image=123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest"
Terraform te va a mostrar todo lo que planea crear antes de hacer nada. Revisa el plan con
cuidado, despues escribi yes para continuar. El primer deployment tarda unos minutos porque
necesita crear la VPC, subnets, NAT gateway, ALB y recursos de ECS.
Cuando termine, Terraform va a imprimir los outputs. Agarra el valor de alb_dns_name, ese
es tu endpoint de la API.
Testeando el deployment
Verifiquemos que todo esta funcionando. Usa el DNS name del ALB del output de Terraform:
# Chequear el endpoint de health
curl http://task-api-alb-123456789.us-east-1.elb.amazonaws.com/health
# Respuesta esperada:
# {"status":"healthy","uptime":42.123,"timestamp":"2026-05-12T10:30:00.000Z"}
Proba crear un task:
# Crear un nuevo task
curl -X POST \
http://task-api-alb-123456789.us-east-1.elb.amazonaws.com/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Deploy a ECS", "description": "Primer task desde produccion!"}'
# Listar todos los tasks
curl http://task-api-alb-123456789.us-east-1.elb.amazonaws.com/tasks
Si todo esta funcionando, deberias ver respuestas saludables. Si algo esta mal, revisa los logs de CloudWatch:
# Ver logs recientes de los tasks de ECS
aws logs tail /ecs/task-api --follow --since 10m
Tambien podes chequear los eventos del servicio ECS para ver si los tasks estan iniciando y parando correctamente:
aws ecs describe-services \
--cluster task-api-cluster \
--services task-api-service \
--query 'services[0].events[:10]' \
--output table
Deployeando actualizaciones: el flujo de rolling deployment
Cuando pusheas una nueva version de tu imagen Docker, necesitas decirle a ECS que la agarre. La forma mas simple es forzar un nuevo deployment:
# Construir, etiquetar y pushear la nueva imagen
docker build -t task-api .
docker tag task-api:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest
docker push \
123456789012.dkr.ecr.us-east-1.amazonaws.com/task-api:latest
# Forzar a ECS a pullear la nueva imagen
aws ecs update-service \
--cluster task-api-cluster \
--service task-api-service \
--force-new-deployment
Esto es lo que pasa durante un rolling deployment:
- ECS inicia nuevos tasks con la imagen actualizada junto a los existentes (hasta
deployment_maximum_percent)- Los health checks del ALB verifican que los nuevos tasks estan sanos
- Una vez que los nuevos tasks pasan los health checks, el ALB empieza a rutear trafico hacia ellos
- ECS drena las conexiones de los tasks viejos (respetando
deregistration_delay)- Los tasks viejos se detienen
- Si los nuevos tasks no logran ponerse saludables, el deployment circuit breaker automaticamente hace rollback a la version anterior
Todo este proceso ocurre con cero downtime. Tus usuarios nunca ven un error durante el deployment porque los tasks viejos siguen sirviendo trafico hasta que los nuevos estan listos.
En un pipeline CI/CD real (que cubrimos antes en la serie), automatizarias todo este flujo. Push a main, CI construye la imagen, pushea a ECR, y dispara el deployment en ECS. Sin pasos manuales.
Consideraciones de costos
Antes de cerrar, hablemos de cuanto cuesta esto. El pricing de Fargate se basa en la CPU y memoria que asignas a cada task, facturado por segundo con un minimo de un minuto:
- 0.25 vCPU, 512 MiB (nuestra configuracion): aproximadamente $0.01/hora por task
- Con 2 tasks corriendo 24/7: aproximadamente $15/mes de computo
- NAT gateway: alrededor de $32/mes (esto suele ser el costo mas grande para deployments chicos)
- ALB: aproximadamente $16/mes mas transferencia de datos
- ECR: $0.10/GB/mes de almacenamiento, los primeros 500 MB gratis
- CloudWatch Logs: $0.50/GB ingestado
Para una API chica, estas mirando aproximadamente $65-80/mes en total. El NAT gateway es el
componente individual mas caro. Si el costo es una preocupacion, podrias correr tus tasks en
subnets publicas con assign_public_ip = true y saltarte el NAT gateway, pero esto no es
recomendado para workloads de produccion porque expone tus tasks directamente a internet.
Notas finales
Ahora tenes un deployment production-grade de tu API TypeScript en AWS ECS con Fargate. El setup incluye una VPC apropiada con subnets publicas y privadas, un Application Load Balancer para distribucion de trafico y health checking, auto-scaling para manejar carga variable, un deployment circuit breaker para seguridad, y logging centralizado en CloudWatch. Todo gestionado como codigo con Terraform.
ECS con Fargate es una excelente eleccion cuando queres orquestacion de containers sin la complejidad de Kubernetes. Se integra estrechamente con el ecosistema AWS, requiere minimo overhead operativo, y escala bien para la mayoria de los workloads.
En el proximo articulo vamos a ver servicios AWS mas avanzados y prepararnos para el salto a Kubernetes con EKS. Si seguiste hasta aca, ya entendes los fundamentos de orquestacion de containers, lo que va a hacer que Kubernetes sea mucho mas facil de aprender.
Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!
Errata
Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.
Tambien podes revisar el codigo fuente y los cambios en las fuentes aca
$ Comentarios
Online: 0Por favor inicie sesión para poder escribir comentarios.