AWS Deploy
An optional path — for public access without running your own reverse proxy, AWS’s built-in security, or simply to learn AWS hosting. Costs roughly $0/month for personal use.
Prerequisites
Section titled “Prerequisites”- A domain name you own. There is no way around this: CloudFront needs an ACM certificate, and ACM only issues one for a domain you can prove you control. Register one at any registrar — DNS records are added by hand (see below), so any provider works.
- An AWS account
- AWS CLI configured with credentials
- Terraform >= 1.5
- Node.js >= 20
- uv (packages the Lambda and runs the user scripts)
- Task (task runner)
You will add two DNS records by hand during setup: one to validate the certificate, one to point your subdomain at CloudFront. Both are plain CNAMEs.
Configure your deployment
Section titled “Configure your deployment”Terraform reads deployment-specific values from infra/terraform.tfvars, which is
gitignored so nobody’s domain or AWS account id ends up in version control. Do this before
the first task apply:
cp infra/terraform.tfvars.example infra/terraform.tfvarsdomain is the only required variable. A file containing just domain = "example.com"
is enough for a first deploy; there is no separate Lambda environment setup — infra/lambda.tf
derives everything from these variables.
Worth setting before the first apply
Section titled “Worth setting before the first apply”| Variable | Default | Why now rather than later |
|---|---|---|
subdomain | mcgamertime | Gives mcgamertime.example.com. Changing it later means a new certificate and a new DNS record |
bgg_token | empty | Enables Board Game Geek search (apply for a token). Stored in SSM with lifecycle.ignore_changes, so it is read only on the first apply; adding it afterwards means editing the SSM parameter by hand |
aws_region | eu-west-1 | Where Lambda, DynamoDB and S3 land. Moving region later means recreating everything, data included |
Optional, changeable any time
Section titled “Optional, changeable any time”| Variable | Default | What it does |
|---|---|---|
alert_email | empty | Subscribes an address to the CloudWatch alarm topic (Lambda errors, throttles, API Gateway 5xx). AWS emails a confirmation link once — until you click it, alarms fire into a topic nobody reads |
cloudfront_web_acl_arn | empty | ARN of a CloudFront-scoped WAF Web ACL. Empty deploys without a WAF |
extra_cors_origins | [] | Extra credentialed CORS origins beyond the deployed FQDN. The FQDN and the distribution’s own *.cloudfront.net name are always included |
Deploy
Section titled “Deploy”1. Create the Terraform state bucket (one-time)
Section titled “1. Create the Terraform state bucket (one-time)”Terraform state is kept in S3 (infra/backend.tf), never on your laptop: it contains
every secret Terraform has read, and a lost local file means Terraform no longer knows
what it created. The bucket is separate from the site bucket and is created once by hand,
because Terraform cannot store its own state in a bucket it is about to create.
S3 bucket names are global, so the name in backend.tf is taken. Either edit it there, or
copy infra/backend.hcl.example to infra/backend.hcl (gitignored, like terraform.tfvars)
and set yours — every task target picks the override up. Then:
task state:bootstrap # creates the bucket: versioned, encrypted, private2. Provision the infrastructure
Section titled “2. Provision the infrastructure”task init # terraform init (S3 state) + npm citask build # bundle Lambda + build Vite SPAtask plan # review what Terraform will createtask apply # provision infrastructureThe first apply stops at the ACM certificate, which stays PENDING_VALIDATION until you
prove you own the domain — the next step.
3. Validate the certificate (one-time)
Section titled “3. Validate the certificate (one-time)”Get the records ACM wants:
terraform -chdir=infra output acm_validation_recordsAdd each one as a CNAME at whatever DNS provider hosts your domain, using the output’s
name field as the record name and value as the target. Wait ~2 minutes for it to
propagate, then run task apply again — this time it completes.
4. Point your subdomain at CloudFront
Section titled “4. Point your subdomain at CloudFront”terraform -chdir=infra output cloudfront_urlAdd a second CNAME: your subdomain (default mcgamertime) → the *.cloudfront.net value
from that output.
5. Ship the frontend
Section titled “5. Ship the frontend”Once the subdomain resolves to CloudFront:
task deploy # run this every time you want to update your live application6. Create the first admin user
Section titled “6. Create the first admin user”Nothing on the cloud path creates an account for you — ADMIN_USERNAME/ADMIN_PASSWORD
bootstrap only applies to the self-hosted container. Until you run this, the login page
loads and no password works:
task create-user -- --username admin --display-name "Admin" --role admin --password <your-password>The task writes straight to DynamoDB with your local AWS credentials, so run it from a
clone of the repo, after task apply has created the tables. If your aws_region is not
the AWS CLI’s configured default, prefix it: AWS_REGION=<your-region> task create-user -- ....
Then open https://<subdomain>.<your-domain> and log in.
Security and privacy
Section titled “Security and privacy”Uploaded media requires a session, the bucket is not public, and nothing leaves your AWS account. Security & Privacy on AWS documents what is enforced, what is deliberately public, and what you remain responsible for — with commands to verify each claim yourself.
Architecture
Section titled “Architecture”Browser → CloudFront → API Gateway → Lambda → DynamoDB + S3- Frontend: Vite SPA served from S3 via CloudFront
- Backend: FastAPI on Lambda via Mangum + API Gateway HTTP API
- Database: DynamoDB (pay-per-request, free tier covers personal use)
- Storage: S3 (avatars, blog images — free tier covers personal use)
- DNS: a CNAME at your own DNS provider → CloudFront distribution (no Route 53)
- TLS: ACM certificate (us-east-1, CloudFront requirement)