AWS Infrastructure
How to deploy and manage the AWS infrastructure using OpenTofu
AWS Infrastructure (OpenTofu)
1. Bootstrap state backend (run once per AWS account/region)
This creates the S3 bucket and DynamoDB table used for OpenTofu/Terraform state and locking.
cd infra/bootstrap
tofu init
tofu apply2. Local deployment
Create infra/.env (not committed) with your AWS credentials, for example:
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=eu-central-1TODO: Improve AWS credential handling
Long-term we should replace static access key/secret credentials with:
- An AWS IAM role assumed via GitHub OIDC for CI deployments
- AWS SSO / role-based auth (profiles) for local development
Then from the repo root:
make docker-build
make docker-push
make plan # review the plan output
make apply # confirm and applyThis will:
- Build the app with Bun
- Build and push the Docker image to ECR
- Run
tofu planininfra/(viamake plan) - Run
tofu applyininfra/(viamake apply) - Print the ALB DNS name after apply
3. Getting the ALB URL
After make apply finishes, the Makefile prints:
ALB DNS: <alb-dns-name>You can open http://<alb-dns-name>/ in your browser.
4. Destroying infrastructure
To destroy the main application infrastructure (Aurora, ECS, ALB, VPC, etc.):
make infra-destroyThis runs tofu destroy in infra/.
If you also want to remove the state backend itself (S3 bucket and DynamoDB table), run this separately:
cd infra/bootstrap
tofu destroy5. Initial setup gotcha (first deploy only)
NOTE: This section only applies to the first deploy. Future deploys can skip to step 3.
On the very first deploy, some infrastructure does not exist yet (S3 state bucket, DynamoDB lock table, and the ECR repository). The initial sequence should be:
-
Bootstrap state backend:
cd infra/bootstrap tofu init tofu apply -
Create core infra (VPC, ECS, ALB, Aurora, ECR, etc.):
cd ../.. make infra-init make plan make apply -
Build and push the Docker image (now that the ECR repo exists):
make docker-build make docker-push -
Run
make plan/make applyagain to roll the ECS service to the newly pushed image.
After this first run, future deploys normally only need to rebuild/push and run make plan / make apply.
6. DNS / HTTPS stack (infra/dns)
The DNS stack manages the public subdomain ppaai.info.nl in Route 53 and is used for both DNS and TLS:
- Creates a public hosted zone for
ppaai.info.nl. - Is consumed by
infra/dns_acm.tfto request ACM certificates forppaai.info.nland*.ppaai.info.nlwith DNS validation. - Exposes the hosted zone name servers so you can delegate the subdomain from the parent
info.nlzone (PowerDNS).
From the repo root:
make dns-init # one-time backend init for infra/dns
make dns-plan # optional
make dns-apply # create/ update Route 53 hosted zone and ACM validation recordsAfter make dns-apply, run:
cd infra/dns
tofu output ppaai_name_serversThen add these NS records for ppaai.info.nl in your external DNS (PowerDNS) to delegate control of that subdomain to Route 53.
7. Email / SES stack (infra/email)
The email stack configures Amazon SES for the same subdomain and wires all required records into Route 53:
- Creates an SES domain identity for
ppaai.info.nl. - Adds the TXT verification record used by SES to verify the domain.
- Enables DKIM and creates the three DKIM CNAME records in the
ppaai.info.nlRoute 53 zone.
From the repo root:
make email-init # one-time backend init for infra/email
make email-plan # optional
make email-apply # SES domain identity + TXT + DKIM CNAMEsAfter make email-apply, SES in eu-central-1 will list ppaai.info.nl as a verified domain. While your SES account is still in the sandbox, you must also verify any recipient email addresses (or request production access) before sending to them.
8. Testing SES email from the app
The Next.js app includes a small SES helper (apps/web/src/lib/email.ts) and a test route at POST /api/test-email that sends an email via SES.
To test this locally:
-
Ensure SES (region
eu-central-1) has:- Verified domain identity for
ppaai.info.nl(viamake email-apply). - Either a verified recipient email (for sandbox accounts) or production access enabled.
- Verified domain identity for
-
In
apps/web/.envset:AWS_REGION=eu-central-1 EMAIL_FROM=no-reply@ppaai.info.nl TEST_EMAIL_TO=you@yourdomain.com -
Start the dev server from the repo root:
bun run dev -
In another terminal, call the test route:
curl -X POST http://localhost:3000/api/test-verification-email \ -H 'Content-Type: application/json' \ -d '{"to":"you@yourdomain.com"}'If
tois omitted, the route usesTEST_EMAIL_TOfrom the environment.
If the response is { "ok": true } and SES does not reject the recipient, the SES integration is working. You can then reuse the same helper for registration and password-reset emails.