Skip to content

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

LayerService
EdgeApplication Load Balancer (HTTPS via ACM)
ComputeECS Fargate cluster · two services (api, frontend), plus a kyma service when running the Advanced Context Engine
DataRDS Postgres 16 with pgvector, ElastiCache Redis 7
Context EngineBasic — Neo4j AuraDB (recommended) or self-managed EC2 Neo4j. Advancedkyma sidecar service on Fargate, columnar extents on S3, catalog in the same RDS instance.
SecretsAWS Secrets Manager → injected into containers
LogsCloudWatch 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 vector extension installed:
    sql
    CREATE 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 ruleAction
path-pattern: /api/*Forward to agentcy-api target group
DefaultForward to agentcy-frontend target group

Health check paths:

  • agentcy-api/health
  • agentcy-frontend/api/health

Security Groups

SGAllows
sg-albInbound :443 from 0.0.0.0/0
sg-apiInbound :8080 from sg-alb; outbound to sg-rds, sg-redis, internet (NAT)
sg-frontendInbound :3000 from sg-alb; outbound :443 to internet
sg-rdsInbound :5432 from sg-api
sg-redisInbound :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:

  1. Source: GitHub or CodeCommit
  2. Build: CodeBuild — docker buildx build --platform linux/arm64,linux/amd64 --push
  3. 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/api and /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_ENDPOINT to 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

Built by AgentcyLabs. For in-house deployment or Agentcy Cloud (PaaS) access, visit agentcylabs.com.