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.
Prerequisites
Section titled “Prerequisites”-
The boilerplate cloned and running locally (Docker Compose)
-
AWS CLI v2 with admin credentials:
aws sts get-caller-identitysucceeds -
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
What gets created
Section titled “What gets created”| 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).
Configure your domain
Section titled “Configure your domain”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:
{ "context": { "hostedZoneDomain": "example.com", "subdomain": "lt" }}Or pass them on each command:
npx cdk synth --all -c hostedZoneDomain=example.com -c subdomain=ltThe commands below use the default projectName, so the cluster is longtail and secrets live under LongTail/. Substitute yours if you change it.
First deploy
Section titled “First deploy”-
Install and build the CDK app:
Terminal window cd deploy/cdknpm cinpm run build -
Synthesize. The only AWS call is the hosted zone lookup:
Terminal window npx cdk synth --all -
Deploy the network (about 5 minutes):
Terminal window npx cdk deploy LongTail-Network --require-approval never -
Deploy the data stack (about 15 minutes):
Terminal window npx cdk deploy LongTail-Data --require-approval never -
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 -
Deploy DNS. The certificate validates through your hosted zone:
Terminal window npx cdk deploy LongTail-Dns --require-approval never -
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 -
Verify:
Terminal window curl -s https://lt.example.com/healthaws ecs describe-services --cluster longtail --services api worker \--query 'services[*].{name:serviceName,running:runningCount}' --output table -
Read the generated superadmin password and sign in at
https://lt.example.com/loginassuperadmin:Terminal window aws secretsmanager get-secret-value --secret-id LongTail/SeedAdminPassword \--query SecretString --output text
Secrets
Section titled “Secrets”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 |
What runs where
Section titled “What runs where”| 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.
Database
Section titled “Database”| 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.
Bucket
Section titled “Bucket”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.
Shell and SQL access
Section titled “Shell and SQL access”Both services run with ECS Exec enabled. Open a shell in an API task:
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:
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.
CI/CD on tags
Section titled “CI/CD on tags”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 |
-
Deploy the OIDC role once. Pass the repository on the command line;
cdk deploy --allskips this stack when these keys are absent:Terminal window cd deploy/cdknpx cdk deploy LongTail-GithubOidc -c githubOwner=OWNER -c githubRepo=REPOThe role trusts only
repo:OWNER/REPO:*and carriesAdministratorAccess. Copy theRoleArnoutput. -
In GitHub, under Settings, Secrets and variables, Actions, add the variables
AWS_ACCOUNT_ID,AWS_REGIONandAWS_ROLE_ARN. -
Point the smoke tests at your domain: set
REMOTE_URLindeploy.ymltohttps://lt.example.comand add the repository secretREMOTE_PASSWORD(the superadmin password). -
Release:
Terminal window git tag v1.0.0 && git push --tags
Day to day
Section titled “Day to day”| 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.
Tear down
Section titled “Tear down”The database cluster and the bucket are retained, and the cluster has deletion protection, so cdk destroy leaves them in place.
npx cdk destroy --all- Turn off deletion protection on the database and delete it.
- Empty and delete the bucket.
- Delete the retained secrets.
