ECS Fargate
After Phase 7 — EC2 → ECS migration, all backend and AI compute runs on AWS ECS Fargate. This page is the canonical inventory of cluster, services, networking, and operational controls.
Cluster
| Property | Value |
|---|---|
| Cluster name | aiqlick |
| Region | eu-north-1 |
| Capacity providers | FARGATE (default), FARGATE_SPOT (allowed for non-prod) |
| Container Insights | Enabled |
| ECS Exec | Enabled on every service (ssmmessages:* granted via task role) |
A single cluster hosts both dev and prod for both services. Per-env isolation is at the IAM, security-group, and SQS-queue level — not at the ECS-cluster level.
Services
| Service | Task definition family | Desired (min/max) | Public ALB | Internal ALB | Cloud Map |
|---|---|---|---|---|---|
backend-dev | aiqlick-backend-dev | 1 / 1 / 4 | tg-backend-dev (host: api-dev.aiqlick.com) | — | backend-dev.aiqlick.local |
backend-prod | aiqlick-backend-prod | 2 / 2 / 8 | tg-backend-prod (host: api.aiqlick.com) | — | backend-prod.aiqlick.local |
bg-tasks-dev | aiqlick-bg-tasks-dev | 1 / 1 / 4 | — | tg-bg-dev (host: bg-dev.aiqlick.local) | bg-dev.aiqlick.local |
bg-tasks-prod | aiqlick-bg-tasks-prod | 2 / 2 / 8 | — | tg-bg-prod (host: bg-prod.aiqlick.local) | bg-prod.aiqlick.local |
All services run in the private subnets (172.31.96.0/20,
172.31.112.0/20, 172.31.128.0/20 across the 3 eu-north-1 AZs).
assignPublicIp=DISABLED — egress goes through VPC endpoints (in-region) or
the NAT gateway (cross-region eu-west-1).
Task sizing
| Task family | CPU | Memory |
|---|---|---|
aiqlick-backend-dev | 0.5 vCPU | 1 GB |
aiqlick-backend-prod | 1 vCPU | 2 GB |
aiqlick-bg-tasks-dev | 0.5 vCPU | 1 GB |
aiqlick-bg-tasks-prod | 1 vCPU | 2 GB |
aiqlick-prisma-migrate | 0.5 vCPU | 1 GB |
Deployment configuration
Every service uses the deployment circuit breaker with auto-rollback:
deploymentCircuitBreaker = { enable: true, rollback: true }
maximumPercent = 200
minimumHealthyPercent = 100
healthCheckGracePeriod = 120s
If the new task revision fails ALB health checks repeatedly, ECS rolls
back to the previous successful deployment automatically. This replaces
the manual :previous tag rollback the EC2 BG workflow used.
Autoscaling
Application Auto Scaling target tracking on
ECSServiceAverageCPUUtilization = 70%.
| Service | Min | Max |
|---|---|---|
backend-dev | 1 | 4 |
backend-prod | 2 | 8 |
bg-tasks-dev | 1 | 4 |
bg-tasks-prod | 2 | 8 |
Scale-out cooldown 60s, scale-in cooldown 300s — bias toward keeping capacity around longer than strictly necessary, since scale-out is fast (~30s ENI provisioning) and scale-in churn is the bigger UX risk (stops-mid-request, dropped WS connections).
ALB topology
| ALB | Scheme | Listener | Cert | Sticky |
|---|---|---|---|---|
aiqlick-public-alb | internet-facing | HTTPS:443 + HTTP:80 (plain forward, redirect deferred) | ACM api.aiqlick.com SAN api-dev.aiqlick.com (eu-north-1) | Yes (lb_cookie 1h) |
aiqlick-internal-alb | internal | HTTP:80 | None (private VPC, internal-service JWT auth) | No |
Sticky sessions on the public ALB are required for WebSocket subscriptions and SSE — without them a client reconnect under load lands on a different task and loses the in-process subscription state. The internal ALB is request/response so stickiness is unnecessary.
IAM
| Role | Trust principal | Used by |
|---|---|---|
aiqlick-ecs-execution-role-{dev,prod} | ecs-tasks.amazonaws.com | Fargate runtime — pull image, write CloudWatch logs, read Secrets Manager bundle |
aiqlick-ecs-backend-task-role-{dev,prod} | ecs-tasks.amazonaws.com | Backend app — S3 (uploads bucket), SQS (mail / notifications / inbound), SNS (pubsub fanout), EventBridge PutEvents, AppConfig read, X-Ray |
aiqlick-ecs-bg-task-role-{dev,prod} | ecs-tasks.amazonaws.com | BG app — Bedrock / Rekognition / Transcribe / Polly, S3, SQS (inbound + agent-docs + bg-events), SNS publish, AppConfig read, X-Ray |
aiqlick-scheduler-role | scheduler.amazonaws.com | EventBridge Scheduler — invokes aiqlick-cron-destination API destination for cron POSTs |
github-actions-role | OIDC (GitHub) | CI deploys — ECR push, ECS RegisterTaskDefinition / UpdateService, ECS RunTask for prisma-migrate |
Each task role's full policy is committed alongside the role name at
aiqlick-backend/infrastructure/ecs/iam/
and background-tasks/infrastructure/ecs/iam/.
Logging
CloudWatch Log groups (30-day retention):
/ecs/aiqlick-backend-{dev,prod}/ecs/aiqlick-bg-tasks-{dev,prod}/ecs/aiqlick-prisma-migrate
Streams are named task/{containerName}/{taskId} so per-task isolation
is straightforward to filter on.
VPC endpoints
To keep Fargate tasks off the public internet for AWS service traffic, nine interface endpoints + one S3 gateway endpoint are attached to the private subnets:
ecr.api,ecr.dkr— image pulllogs— awslogs driversecretsmanager— task-def secret resolutionsqs,sns,events— async messaging planeappconfig,appconfigdata— feature flag pollingbedrock-runtime— BG LLM calls (in-region)s3(Gateway) — uploads bucket access (free)
Cross-region calls (Rekognition / Transcribe / Polly in eu-west-1) go via the NAT gateway in AZ-A — there's no eu-north-1 endpoint available for those services.
Cost (per month, eu-north-1 list price)
| Component | $/month |
|---|---|
| 4× Fargate avg 0.75 vCPU / 1.5 GB | ~80 |
| Public ALB | ~22 |
| Internal ALB | ~22 |
| NAT gateway | ~32 |
| 9× Interface VPC endpoints | ~50 |
| Total | ~206 |
Common operations
# Health snapshot of all services
aws ecs describe-services --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick \
--services backend-prod backend-dev bg-tasks-prod bg-tasks-dev \
--query 'services[*].{name:serviceName,running:runningCount,desired:desiredCount,rollout:deployments[0].rolloutState}' \
--output table
# Open a shell in a running task (replaces SSH)
TASK=$(aws ecs list-tasks --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --service-name backend-dev --query 'taskArns[0]' --output text)
aws ecs execute-command --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --task "$TASK" --container aiqlick-backend \
--interactive --command '/bin/sh'
# Tail logs
aws logs tail --profile Administrator-842697652860 --region eu-north-1 \
--follow --format short /ecs/aiqlick-backend-prod
# Force redeploy (no code change)
aws ecs update-service --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --service backend-prod --force-new-deployment
# Rollback to a previous task definition revision
aws ecs update-service --profile Administrator-842697652860 --region eu-north-1 \
--cluster aiqlick --service backend-prod \
--task-definition aiqlick-backend-prod:42