AWS
A reference layout on ECS Fargate with RDS for PostgreSQL, and the pitfalls specific to AWS.
This page describes the pieces and their settings, not a ready-made template: every AWS account has its own network, naming and tooling. Translate it into Terraform, CloudFormation, CDK or the console as you prefer.
Layout
| Part | AWS service | Settings |
|---|---|---|
| Database | RDS for PostgreSQL 16–18, or an external provider | db.t4g.small or larger, 20 GB gp3. The data takes about 2.3 GB. |
serve |
ECS service on Fargate behind an Application Load Balancer | 0.25 vCPU, 0.5 GB, one or more tasks. |
sync |
ECS task on Fargate, started by EventBridge Scheduler | 0.5 vCPU, 1 GB, schedule rate(1 hour). |
migrate |
The same task definition as sync, run once per release |
Command migrate, with the migrate role. |
| Secrets | Secrets Manager | One secret per database role, plus the API key hashes. |
| Logs | CloudWatch Logs (awslogs) |
The logs hold no names, queries or identifiers. |
Instead of the scheduled task, sync can run as an ECS service with one task and the command sync --every 1h.
Both are correct; the scheduled task costs nothing between runs.
Steps
Database
Create the instance in private subnets. pg_trgm is available on RDS without any parameter change. Connect as
the master user once and create the three roles from Database. On RDS the master
user can do this: it does not need to be a true superuser.
Secrets
Store REGISTER_MIGRATE_DATABASE_URL, REGISTER_SYNC_DATABASE_URL, REGISTER_DATABASE_URL and
REGISTER_API_KEYS in Secrets Manager. Give each task definition only the secret its command needs, through the
container’s secrets field; the task execution role needs secretsmanager:GetSecretValue on those secrets only.
Migrate
Run a one-off task with the command migrate and the migrate secret, and wait for exit code 0.
Sync
Create the schedule. The first run downloads the full RPO snapshot and takes about 15 minutes; later runs take seconds to a few minutes. An overlapping start exits at once, so a long first run never collides with the next one.
Serve
Create the service and the load balancer, with TLS on the listener. The API answers 503 until the first sync
has published every source.
Container definitions
The relevant parts, with placeholders in angle brackets:
{
"name": "sync",
"image": "<image>",
"command": ["sync"],
"secrets": [
{ "name": "REGISTER_SYNC_DATABASE_URL", "valueFrom": "<sync secret ARN>" }
],
"environment": [
{ "name": "REGISTER_WORK_DIR", "value": "/tmp/accquix-register" }
],
"logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "<group>", "awslogs-region": "<region>", "awslogs-stream-prefix": "sync" } }
}
{
"name": "serve",
"image": "<image>",
"command": ["serve"],
"portMappings": [{ "containerPort": 8080 }],
"secrets": [
{ "name": "REGISTER_DATABASE_URL", "valueFrom": "<api secret ARN>" },
{ "name": "REGISTER_API_KEYS", "valueFrom": "<api keys secret ARN>" }
]
}
Pitfalls
| Pitfall | What to do |
|---|---|
Load balancer health check on /readyz. /readyz is 503 until the first sync has finished, so the target group marks every task unhealthy and ECS replaces them in a loop. |
Use /livez for the target group health check. Watch /readyz and /v1/sources in your monitoring instead. |
Container health check. The image is distroless: it has no shell and no curl, so a CMD-SHELL health check always fails. |
Leave the container health check out and rely on the load balancer. |
RDS Proxy for sync. The proxy pins or breaks the advisory lock and COPY. |
Point REGISTER_SYNC_DATABASE_URL at the instance endpoint. serve may use the proxy. |
sslmode=verify-full on RDS. RDS certificates are signed by the RDS certificate authority, which is not in the image’s trust store, so the connection fails. |
Build a derived image that adds the RDS CA bundle and set sslrootcert=<path> in the URL, or use sslmode=require inside a private VPC. |
Read-only root file system. With readonlyRootFilesystem, sync cannot write its work directory. |
Mount a task volume (ephemeral storage is enough) and point REGISTER_WORK_DIR at it. |
App Runner or Lambda for sync. Both stop work that is not a request; the first sync runs for about 15 minutes. |
Use ECS tasks for sync. |
| No outbound internet. Tasks in private subnets without a NAT gateway or a public IP cannot reach the sources. | Add a NAT gateway, or run the tasks in public subnets with a public IP and a strict security group. |
| External database with an IP allow list. Fargate tasks get changing addresses. | Send the traffic through a NAT gateway with an Elastic IP and allow that address. |
Updates
- Publish the new image tag.
- Run the
migratetask with the new image and wait for exit code 0. - Update both task definitions to the new image. The next scheduled
syncuses it;serverolls out through the ECS deployment.