Skip to content

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.

  • 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.

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:

Terminal window
cp infra/terraform.tfvars.example infra/terraform.tfvars

domain 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.

VariableDefaultWhy now rather than later
subdomainmcgamertimeGives mcgamertime.example.com. Changing it later means a new certificate and a new DNS record
bgg_tokenemptyEnables 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_regioneu-west-1Where Lambda, DynamoDB and S3 land. Moving region later means recreating everything, data included
VariableDefaultWhat it does
alert_emailemptySubscribes 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_arnemptyARN 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

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:

Terminal window
task state:bootstrap # creates the bucket: versioned, encrypted, private
Terminal window
task init # terraform init (S3 state) + npm ci
task build # bundle Lambda + build Vite SPA
task plan # review what Terraform will create
task apply # provision infrastructure

The first apply stops at the ACM certificate, which stays PENDING_VALIDATION until you prove you own the domain — the next step.

Get the records ACM wants:

Terminal window
terraform -chdir=infra output acm_validation_records

Add 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.

Terminal window
terraform -chdir=infra output cloudfront_url

Add a second CNAME: your subdomain (default mcgamertime) → the *.cloudfront.net value from that output.

Once the subdomain resolves to CloudFront:

Terminal window
task deploy # run this every time you want to update your live application

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:

Terminal window
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.

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.

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)