Appearance
AWS — ECS Fargate
The fastest serverless path on AWS without Kubernetes. This guide is the building-block view — for a single-template provisioner see AWS CloudFormation, or use these task definitions inside your existing Terraform / CDK stack.
Architecture
| Layer | Service |
|---|---|
| Edge | Application Load Balancer (HTTPS via ACM) |
| Compute | ECS Fargate cluster · two services (api, frontend), plus a kyma service when running the Advanced Context Engine |
| Data | RDS Postgres 16 with pgvector, ElastiCache Redis 7 |
| Context Engine | Basic — Neo4j AuraDB (recommended) or self-managed EC2 Neo4j. Advanced — kyma sidecar service on Fargate, columnar extents on S3, catalog in the same RDS instance. |
| Secrets | AWS Secrets Manager → injected into containers |
| Logs | CloudWatch Logs /agentcy/* |
The frontend and API run as separate ECS services so you can scale and roll them independently. The ALB routes /api/* to the API target group and everything else to the frontend.
Prerequisites
- A VPC with at least two private subnets and two public subnets (or use the default VPC for testing)
- An ECS cluster (Fargate, no EC2 capacity providers needed)
- RDS Postgres 16 with the
vectorextension installed:sqlCREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; - ElastiCache Redis 7 reachable from the API task subnet
- A reachable Context Engine: either an AuraDB / self-hosted Neo4j (Basic), or a kyma service on the same VPC and an S3 bucket for extents (Advanced — see kyma docs)
Task Definition — API
json
{
"family": "agentcy-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::123456789012:role/agentcy-task-execution",
"taskRoleArn": "arn:aws:iam::123456789012:role/agentcy-task",
"runtimePlatform": {
"cpuArchitecture": "ARM64",
"operatingSystemFamily": "LINUX"
},
"containerDefinitions": [
{
"name": "agentcy-api",
"image": "ghcr.io/agentcy/backend:latest",
"essential": true,
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"environment": [
{ "name": "BIND_ADDR", "value": "0.0.0.0:8080" },
{ "name": "LLM_PROVIDER", "value": "anthropic" },
{ "name": "AUTH_PROVIDER", "value": "local" },
{ "name": "LOG_LEVEL", "value": "info" },
{ "name": "CORS_ORIGINS", "value": "https://agentcy.example.com" }
],
"secrets": [
{ "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/postgres-url-XXXX" },
{ "name": "REDIS_URL", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/redis-url-XXXX" },
// --- Basic Context Engine (Neo4j-compatible) ---
{ "name": "NEO4J_URI", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/neo4j:uri::" },
{ "name": "NEO4J_USER", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/neo4j:username::" },
{ "name": "NEO4J_PASSWORD", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/neo4j:password::" },
// --- OR Advanced Context Engine (kyma on S3) — drop the NEO4J_* trio above and use:
// { "name": "KYMA_BASE_URL", "valueFrom": ".../agentcy/kyma:base_url::" },
// { "name": "KYMA_TOKEN", "valueFrom": ".../agentcy/kyma:token::" },
// and set "CONTEXT_ENGINE=advanced", "KYMA_DATABASE=kyma" in environment.
{ "name": "LLM_API_KEY", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/llm-api-key-XXXX" },
{ "name": "JWT_SECRET", "valueFrom": "arn:aws:secretsmanager:us-east-1:123:secret:agentcy/jwt-XXXX" }
],
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"],
"interval": 15,
"timeout": 5,
"retries": 3,
"startPeriod": 30
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/agentcy/api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
}
}
]
}The backend image is published as a multi-arch manifest (amd64 + arm64). ARM64 Fargate is roughly 20% cheaper for the same throughput.
Task Definition — Frontend
json
{
"family": "agentcy-frontend",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/agentcy-task-execution",
"runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" },
"containerDefinitions": [
{
"name": "agentcy-frontend",
"image": "ghcr.io/agentcy/frontend:latest",
"essential": true,
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [
{ "name": "NEXT_PUBLIC_API_URL", "value": "https://agentcy.example.com/api/v1" },
{ "name": "NODE_ENV", "value": "production" }
],
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"],
"interval": 15,
"timeout": 5,
"retries": 3
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/agentcy/frontend",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
}
}
]
}Optional — kyma sidecar service (Advanced provider)
Skip this section if you're running the Basic provider (Neo4j / AuraDB).
For the Advanced Context Engine, run kyma as a third ECS service. It needs an S3 bucket for columnar extents and uses the existing RDS instance for its catalog.
json
{
"family": "agentcy-kyma",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "4096",
"executionRoleArn": "arn:aws:iam::123456789012:role/agentcy-task-execution",
"taskRoleArn": "arn:aws:iam::123456789012:role/agentcy-kyma-task",
"containerDefinitions": [
{
"name": "kyma",
"image": "ghcr.io/agentcylabs/kyma:latest",
"essential": true,
"portMappings": [
{ "containerPort": 8080, "protocol": "tcp" },
{ "containerPort": 4317, "protocol": "tcp" }
],
"environment": [
{ "name": "KYMA_OBJECT_STORE_URL", "value": "s3://agentcy-kyma-prod" },
{ "name": "KYMA_OTLP_ADDR", "value": "0.0.0.0:4317" },
{ "name": "AWS_REGION", "value": "us-east-1" }
],
"secrets": [
{ "name": "KYMA_CATALOG_DATABASE_URL", "valueFrom": ".../agentcy/kyma-catalog-url-XXXX" },
{ "name": "KYMA_TOKEN", "valueFrom": ".../agentcy/kyma:token::" }
],
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"],
"interval": 15, "timeout": 5, "retries": 3, "startPeriod": 30
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/agentcy/kyma",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}agentcy-kyma-task IAM role needs s3:GetObject / PutObject / ListBucket on the kyma bucket. Discovery from the API task is via Service Connect or an internal NLB — set KYMA_BASE_URL accordingly.
Register and Run
bash
aws ecs register-task-definition --cli-input-json file://api.json
aws ecs register-task-definition --cli-input-json file://frontend.json
aws ecs create-service \
--cluster agentcy \
--service-name agentcy-api \
--task-definition agentcy-api \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-api],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/agentcy-api/...,containerName=agentcy-api,containerPort=8080" \
--health-check-grace-period-seconds 60
aws ecs create-service \
--cluster agentcy \
--service-name agentcy-frontend \
--task-definition agentcy-frontend \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-frontend],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/agentcy-frontend/...,containerName=agentcy-frontend,containerPort=3000"ALB Routing
Two target groups, one listener with a rule:
| Listener rule | Action |
|---|---|
path-pattern: /api/* | Forward to agentcy-api target group |
| Default | Forward to agentcy-frontend target group |
Health check paths:
agentcy-api→/healthagentcy-frontend→/api/health
Security Groups
| SG | Allows |
|---|---|
sg-alb | Inbound :443 from 0.0.0.0/0 |
sg-api | Inbound :8080 from sg-alb; outbound to sg-rds, sg-redis, internet (NAT) |
sg-frontend | Inbound :3000 from sg-alb; outbound :443 to internet |
sg-rds | Inbound :5432 from sg-api |
sg-redis | Inbound :6379 from sg-api |
Auto-scaling
Register the API service with Application Auto Scaling:
bash
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/agentcy/agentcy-api \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 \
--max-capacity 10
aws application-autoscaling put-scaling-policy \
--policy-name agentcy-api-cpu \
--service-namespace ecs \
--resource-id service/agentcy/agentcy-api \
--scalable-dimension ecs:service:DesiredCount \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 65.0,
"PredefinedMetricSpecification": {"PredefinedMetricType": "ECSServiceAverageCPUUtilization"}
}'CI/CD with CodePipeline (sketch)
A typical pipeline:
- Source: GitHub or CodeCommit
- Build: CodeBuild —
docker buildx build --platform linux/arm64,linux/amd64 --push - Deploy: ECS rolling deploy via
aws ecs update-service --force-new-deployment
Or use Blue/Green via CodeDeploy with DeploymentController: CODE_DEPLOY on the service — gets you traffic shifting and automatic rollback.
Observability
- Logs → CloudWatch Logs groups
/agentcy/apiand/agentcy/frontend. Add a Logs Insights saved query for[level=ERROR]. - Metrics → ECS service metrics in CloudWatch out of the box. The API also exposes
/metrics(Prometheus) — scrape with the AWS Distro for OpenTelemetry sidecar if you push metrics to AMP. - Traces → set
OTEL_EXPORTER_OTLP_ENDPOINTto your collector URL.
Troubleshooting
Tasks restart in a loop
Almost always a failed health check. View the failed task's reason:
bash
aws ecs describe-tasks --cluster agentcy --tasks <task-arn> \
--query 'tasks[0].{stop:stoppedReason,exit:containers[0].exitCode}'Then inspect the CloudWatch log stream — the API logs the reason it's exiting (bad DATABASE_URL, missing pgvector, Neo4j unreachable).
pgvector not found
Connect with psql and run CREATE EXTENSION vector;. Your RDS parameter group must include vector in shared_preload_libraries (default for PG 16 on RDS — confirm if you customized the parameter group).
High Fargate cost
Switch tasks to ARM64 and right-size CPU/memory. The API's p99 footprint for a Team-tier workload is typically 0.5 vCPU / 768 MB.
Next Steps
- AWS CloudFormation — same architecture as a single template
- AWS EKS — when you outgrow Fargate or want fleet-wide GitOps
- Architecture & Tech Stack — managed-service equivalents and tradeoffs