Ho distribuito e gestito molti servizi containerizzati su ECS Fargate. Nel tempo è emerso un insieme di pattern che applico sistematicamente a ogni nuovo servizio. Questo articolo documenta quei pattern con esempi Terraform, coprendo tutto dalle strategie Fargate Spot ai circuit breaker di deployment e alla migrazione ARM64.
L’Architettura Standard#
Ogni servizio che distribuisco segue la stessa architettura di alto livello:
flowchart LR
Internet([Internet / VPC]) --> WAF[WAF\nrate limiting]
WAF --> ALB[ALB\nHTTPS / TLS 1.3]
ALB --> Fargate[ECS Fargate\nARM64 Spot+OnDemand]
Fargate --> Aurora[(Aurora PostgreSQL\nServerless v2)]
subgraph Security Groups
WAF
ALB
Fargate
Aurora
end
Ogni componente vive nel proprio security group, con il traffico permesso solo dal layer direttamente superiore. L’ALB si trova in subnet private — nessun servizio esposto pubblicamente.
Ogni componente ha il proprio security group, con il traffico che fluisce solo dal layer superiore. L’ALB si trova in subnet private, e Route53 con hosted zone private gestisce il DNS interno.
Strategia Fargate Spot#
Fargate Spot può ridurre i costi di compute fino al 70%, ma è necessario gestire le interruzioni. L’approccio: usare una strategia di capacity provider con pesi bilanciati tra risparmio e disponibilità.
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = var.spot_weight
base = 1 # Almeno 1 task su Fargate On-Demand
}
default_capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = var.ondemand_weight
}
}Il base = 1 su FARGATE garantisce che ci sia sempre almeno un task in esecuzione su capacità On-Demand. Questo è la rete di sicurezza durante le interruzioni Spot.
Per non-produzione uso un rapporto Spot/OnDemand di 4:1. Per produzione lo capovolgo a 1:4, privilegiando la stabilità pur ottenendo qualche risparmio Spot.
Circuit Breaker per i Deployment#
I circuit breaker di deployment ECS eseguono automaticamente il rollback dei deployment falliti. Combinati con la giusta configurazione degli health check, prevengono che deployment errati abbattano il servizio:
resource "aws_ecs_service" "main" {
name = var.service_name
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = var.desired_count
deployment_circuit_breaker {
enable = true
rollback = true
}
deployment_configuration {
maximum_percent = 200
minimum_healthy_percent = 100
}
}maximum_percent = 200 con minimum_healthy_percent = 100 significa che ECS avvierà nuovi task prima di drenare quelli vecchi (rolling deployment). Se i nuovi task falliscono gli health check, il circuit breaker interviene e fa il rollback.
Configurazione degli Health Check#
Impostare correttamente gli health check è critico. Troppo aggressivi e ottieni falsi positivi; troppo permissivi e i deployment falliti impiegano un’eternità per essere rilevati:
resource "aws_lb_target_group" "main" {
health_check {
enabled = true
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 10
interval = 30
matcher = "200"
}
deregistration_delay = 30
}Alcune note:
deregistration_delay = 30invece dei 300 secondi predefiniti. La maggior parte delle applicazioni può drenare le richieste in-flight in 30 secondi, e il ritardo più breve significa deployment più veloci.healthy_threshold = 2significa che un task ha bisogno di soli 2 health check riusciti per essere considerato sano (60 secondi con intervallo di 30 secondi).
Migrazione ad ARM64 (Graviton)#
Le istanze AWS Graviton offrono circa il 20% di migliore price-performance rispetto a x86. Migrare i task ECS Fargate ad ARM64 è semplice se le immagini lo supportano:
# Dockerfile multi-arch
FROM --platform=$TARGETPLATFORM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Build e push di immagini multi-arch:
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t $ECR_REPO:latest \
--push .Poi aggiorna la task definition:
resource "aws_ecs_task_definition" "main" {
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "ARM64"
}
# ...
}La riga chiave è cpu_architecture = "ARM64". Fine. Se la tua immagine Docker è multi-arch, Fargate scarica automaticamente l’architettura giusta.
Auto-Scaling#
I servizi ECS dovrebbero scalare sia su CPU che su memoria. Uso policy di target tracking:
resource "aws_appautoscaling_policy" "cpu" {
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}I cooldown asimmetrici sono importanti: scale_out_cooldown = 60 significa che il servizio reagisce rapidamente ai picchi di carico, mentre scale_in_cooldown = 300 previene lo scale-down prematuro durante traffico a raffiche.
Il Pattern Completo#
Ecco il pattern completo per un nuovo servizio:
- Repository ECR con lifecycle policy (mantieni ultime 10 immagini)
- ECS cluster con Container Insights abilitato
- Task definition con ARM64, limiti di risorse appropriati, iniezione segreti
- Servizio ECS con circuit breaker, strategia Spot, auto-scaling
- ALB con HTTPS (TLS 1.3), routing basato su path
- WAF con rate limiting e regole AWS managed
- Aurora Serverless v2 con scaling appropriato all’ambiente
- Route53 record nella hosted zone privata
- CloudWatch log group con retention 14 giorni
- Security group con modello a tre tier (ALB -> ECS -> Aurora)
Una volta che hai questo come insieme di moduli Terraform, distribuire un nuovo servizio è semplicemente comporre i moduli con variabili specifiche del servizio. L’infrastruttura è coerente, sicura e ottimizzata per i costi in tutti gli ambienti.