Skip to content

AWS Fargate

The boilerplate ships its AWS infrastructure as CDK code in deploy/cdk/. At the end of this page https://lt.example.com serves the dashboard and API from Fargate, a separate worker service executes workflows, both share an Aurora PostgreSQL cluster, and a git tag deploys a new release.

  • The boilerplate cloned and running locally (Docker Compose)

  • AWS CLI v2 with admin credentials: aws sts get-caller-identity succeeds

  • Node 22 or later

  • Docker running (CDK builds the image locally, for linux/amd64)

  • A Route 53 hosted zone for your domain, already created

  • CDK bootstrapped for your account and region:

    Terminal window
    npx cdk bootstrap aws://ACCOUNT_ID/REGION
Stack Resources
LongTail-Network VPC across 2 AZs with public, private and isolated subnets, 1 NAT gateway, security groups for the ALB, API, workers, NATS and database
LongTail-Data Aurora Serverless v2 PostgreSQL 16, an S3 bucket, Secrets Manager entries
LongTail-Dns Hosted zone lookup and a DNS-validated ACM certificate for your subdomain
LongTail-Compute ECS cluster, Fargate services api, worker and nats, an internet-facing ALB, the Route 53 A record
LongTail-GithubOidc An IAM role GitHub Actions assumes through OIDC. Deployed once, separately.

All resources carry Project and Environment tags.

Internet
│
┌───┴───┐
│ ALB │ public subnets: 80 → 443, 443, 9222
└───┬───┘
┌─────────────┼─────────────┐
│ :3030 │ :9222 │
┌────┴────┐ ┌────┴────┐ ┌────┴─────┐
│ API │ │ NATS │ │ Worker │ private subnets, NAT egress
│ Fargate │ │ Fargate │ │ Fargate │
└────┬────┘ └────┬────┘ └────┬─────┘
│ :4222 │ :4222 │
└─────────────┼─────────────┘
│ :5432
┌────┴────┐
│ Aurora │ isolated subnets, no internet route
│ Postgres│
└─────────┘
Security group Inbound
ALB 80, 443, 9222 from anywhere
API tasks 3030 from the ALB
Worker tasks none
NATS 4222 from API and workers; 9222 and 8222 (health) from the ALB
Database 5432 from API and workers

The dashboard’s live updates reach NATS through the API: Long Tail proxies wss://<your domain>/nats-ws to ws://nats:9222 inside the VPC (NATS_WS_TARGET).

deploy/cdk/lib/config.ts reads every deployment-specific value from CDK context:

Context key Default Used for
projectName LongTail Stack prefix (LongTail-Network), secret paths (LongTail/...)
projectSlug lowercase projectName ECS cluster, log groups (/ecs/<slug>/api), S3 bucket, database name and user
hostedZoneDomain the boilerplate’s own zone The Route 53 zone you own
subdomain longtail Record name; the site is <subdomain>.<hostedZoneDomain>
environment production Environment tag
dbName, dbUsername projectSlug Database name and master user

Set your values in deploy/cdk/cdk.json under context, so every command and the CI deploy use the same ones:

deploy/cdk/cdk.json
{
"context": {
"hostedZoneDomain": "example.com",
"subdomain": "lt"
}
}

Or pass them on each command:

Terminal window
npx cdk synth --all -c hostedZoneDomain=example.com -c subdomain=lt

The commands below use the default projectName, so the cluster is longtail and secrets live under LongTail/. Substitute yours if you change it.

  1. Install and build the CDK app:

    Terminal window
    cd deploy/cdk
    npm ci
    npm run build
  2. Synthesize. The only AWS call is the hosted zone lookup:

    Terminal window
    npx cdk synth --all
  3. Deploy the network (about 5 minutes):

    Terminal window
    npx cdk deploy LongTail-Network --require-approval never
  4. Deploy the data stack (about 15 minutes):

    Terminal window
    npx cdk deploy LongTail-Data --require-approval never
  5. Put your model API keys into their secrets before the first boot. CDK creates them with the value placeholder:

    Terminal window
    aws secretsmanager put-secret-value \
    --secret-id LongTail/AnthropicApiKey \
    --secret-string "$ANTHROPIC_API_KEY"
    aws secretsmanager put-secret-value \
    --secret-id LongTail/OpenaiApiKey \
    --secret-string "$OPENAI_API_KEY"

    Confirm each one changed:

    Terminal window
    aws secretsmanager get-secret-value --secret-id LongTail/AnthropicApiKey \
    --query SecretString --output text | head -c 20
  6. Deploy DNS. The certificate validates through your hosted zone:

    Terminal window
    npx cdk deploy LongTail-Dns --require-approval never
  7. Deploy compute. This builds the image, pushes it to ECR and creates the cluster, the services, the ALB and the A record:

    Terminal window
    npx cdk deploy LongTail-Compute --require-approval never
  8. Verify:

    Terminal window
    curl -s https://lt.example.com/health
    aws ecs describe-services --cluster longtail --services api worker \
    --query 'services[*].{name:serviceName,running:runningCount}' --output table
  9. Read the generated superadmin password and sign in at https://lt.example.com/login as superadmin:

    Terminal window
    aws secretsmanager get-secret-value --secret-id LongTail/SeedAdminPassword \
    --query SecretString --output text

ECS injects these into the containers at task start. After you change a value, restart the services.

Secret Created as Action
LongTail/AuroraDatabase Generated database credentials None
LongTail/JwtSigningKey Generated, 64 characters None
LongTail/SeedAdminPassword Generated, 32 characters None; the API seeds superadmin with it on first boot
LongTail/NatsToken Generated, 48 characters None; shared by NATS, API and workers
LongTail/AnthropicApiKey placeholder Replace with your key
LongTail/OpenaiApiKey placeholder Replace with your key
LongTail/OAuthProviders {} Optional: google_client_id, google_client_secret, encryption_key
LongTail/ApiKeys {} Optional
Service Command APP_ROLE CPU Memory Count Scaling
api node build/index.js api 0.5 vCPU 1 GB 1 1 to 4 tasks at 70% CPU
worker node build/index.js worker 0.5 vCPU 1 GB 2 2 to 4 tasks at 70% CPU
nats nats-server 0.25 vCPU 512 MB 1 fixed

Both app services run one image. APP_ROLE picks the mode (Run modes). The worker has a 120-second stop timeout, so a rolling deploy lets in-flight activities finish before the task exits. Rolling deploys keep 100% of tasks healthy and allow 200% during the swap.

Environment Value
NODE_ENV production
PGSSLMODE no-verify
POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD From the database secret
POSTGRES_PORT, POSTGRES_DB 5432, dbName
LT_STORAGE_BACKEND, LT_S3_BUCKET, LT_S3_REGION s3, the stack’s bucket, the stack’s region
BASE_URL https://<your domain>
NATS_URL nats://nats.<slug>.local:4222 (Cloud Map)
NATS_WS_TARGET (API) ws://nats.<slug>.local:9222
LT_OAUTH_ISSUER (API) https://<your domain>

The task role grants read and write on the bucket, so no S3 keys are set.

Setting Value
Engine Aurora PostgreSQL 16.4, Serverless v2
Capacity 0.5 to 8 ACU
Backups 7 days
Deletion protection On; the cluster is retained on stack deletion
Encryption, Performance Insights On
max_connections 200
idle_in_transaction_session_timeout 60000
tcp_keepalives_idle, _interval, _count 60, 10, 6

Services connect directly, without a pooler: LISTEN/NOTIFY needs persistent sessions. See Postgres.

S3-managed encryption, public access blocked, SSL enforced, versioning on, retained on stack deletion. Noncurrent versions expire after 30 days, objects move to Infrequent Access after 60 days, and the tmp/ prefix expires after 7 days.

Both services run with ECS Exec enabled. Open a shell in an API task:

Terminal window
TASK_ID=$(aws ecs list-tasks --cluster longtail --service-name api \
--query 'taskArns[0]' --output text | awk -F/ '{print $NF}')
aws ecs execute-command --cluster longtail --task "$TASK_ID" \
--container api --interactive --command "/bin/sh"

The database sits in isolated subnets with no public route. Reach it from your laptop through SSM port forwarding via an API task:

Terminal window
RUNTIME_ID=$(aws ecs describe-tasks --cluster longtail --tasks "$TASK_ID" \
--query "tasks[0].containers[?name=='api'].runtimeId | [0]" --output text)
DB_HOST=$(aws secretsmanager get-secret-value --secret-id LongTail/AuroraDatabase \
--query SecretString --output text | python3 -c "import sys,json; print(json.load(sys.stdin)['host'])")
aws ssm start-session \
--target "ecs:longtail_${TASK_ID}_${RUNTIME_ID}" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"$DB_HOST\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"5432\"]}"

Both commands need the Session Manager plugin for the AWS CLI. Then connect a SQL client to localhost:5432 as user longtail, database longtail, with the password from the same secret and SSL on.

The boilerplate has two workflows in .github/workflows/:

Workflow Trigger Runs
test.yml Every push and pull request Typecheck, unit tests, integration tests against Docker Compose
deploy.yml Tags matching v*.*.* The tests again, then cdk deploy --all, then smoke tests against the live URL
  1. Deploy the OIDC role once. Pass the repository on the command line; cdk deploy --all skips this stack when these keys are absent:

    Terminal window
    cd deploy/cdk
    npx cdk deploy LongTail-GithubOidc -c githubOwner=OWNER -c githubRepo=REPO

    The role trusts only repo:OWNER/REPO:* and carries AdministratorAccess. Copy the RoleArn output.

  2. In GitHub, under Settings, Secrets and variables, Actions, add the variables AWS_ACCOUNT_ID, AWS_REGION and AWS_ROLE_ARN.

  3. Point the smoke tests at your domain: set REMOTE_URL in deploy.yml to https://lt.example.com and add the repository secret REMOTE_PASSWORD (the superadmin password).

  4. Release:

    Terminal window
    git tag v1.0.0 && git push --tags
Task Command
Deploy a code change cd deploy/cdk && npm run build && npx cdk deploy LongTail-Compute --require-approval never
Preview a change npx cdk diff --all
Tail logs aws logs tail /ecs/longtail/api --follow --format short (or /worker)
Logs from the last 10 minutes aws logs tail /ecs/longtail/worker --since 10m --format short
Restart a service aws ecs update-service --cluster longtail --service worker --force-new-deployment
Service counts aws ecs describe-services --cluster longtail --services api worker --query 'services[*].{name:serviceName,desired:desiredCount,running:runningCount}' --output table

A code change usually touches only task definitions and services: cdk diff shows a handful of resources. More than that is a reason to read the diff before deploying.

To rotate a secret, put-secret-value, then force a new deployment of api and worker. ECS reads secrets only at task start.

The boilerplate’s package.json wraps several of these as npm run aws:deploy, aws:logs:api, aws:logs:worker and aws:services. aws:health curls the boilerplate’s own domain; change it to yours.

The database cluster and the bucket are retained, and the cluster has deletion protection, so cdk destroy leaves them in place.

  1. npx cdk destroy --all
  2. Turn off deletion protection on the database and delete it.
  3. Empty and delete the bucket.
  4. Delete the retained secrets.