case study · cloud engineering

how this site ships — a serverless AWS stack, defined in Terraform.

Every piece of dram-soc.org lives in one Terraform state — the static site, the CDN, the DNS, the certificate, the contact form endpoint, and the log retention policies. `git push` triggers plan + apply. Total spend: roughly $0.80/month.

the diagram

services + purpose

ServicePurposeWhy this one
S3 Origin for the static Astro build. Private bucket. Only CloudFront reads it via OAC; direct S3 URLs return 403.
CloudFront HTTPS-terminating CDN + edge cache. PriceClass_100 (US/EU only) is the cheapest tier that still gives full edge coverage for the target audience.
CloudFront Function URL rewrite: /process → /process/index.html. Cheaper than Lambda@Edge (~$0/mo at this traffic), sub-millisecond.
Response Headers Policy HSTS, X-Frame-Options DENY, Referrer-Policy, XSS-Protection, Content-Type-Options. Managed at the edge instead of in HTML — one place to update, no static-file churn.
Route 53 DNS zone for dram-soc.org — apex + www A-record aliases. Same account as the rest of the stack, aliases are free (unlike CNAME targeting CloudFront).
ACM (us-east-1) TLS certificate for dram-soc.org + www.dram-soc.org. Free. Must live in us-east-1 to attach to CloudFront (single non-negotiable rule).
API Gateway HTTP v2 Public POST /contact endpoint. CORS enforced at gateway. ~70% cheaper than REST API, faster, native OIDC. CORS at gateway saves a Lambda invoke per preflight.
Lambda (Node 20) Validates contact form, writes to DynamoDB, sends via SES. Cold-start < 200ms for a tiny handler. Free tier covers >10× expected traffic.
SES Sends the contact email to my inbox. IAM policy scopes ses:SendEmail to a single verified sender — no impersonation.
DynamoDB Audit log of every contact submission (PITR + encryption at rest). Pay-per-request billing = $0 idle. Cheaper than an RDS instance for <1000 rows.
CloudWatch Logs Lambda logs + HTTP API access logs, 30-day retention. Retention cap avoids infinite log growth cost. Never let default settings ship to prod.

the cost breakdown

Item$/moNotes
Route 53 hosted zone$0.50Fixed regardless of usage
CloudFront egress~$0.20< 10 GB egress at portfolio scale
S3 storage + requests~$0.05Site + logs, ~1 GB total
CloudWatch logs~$0.0530-day retention keeps this bounded
Lambda / API GW / DynamoDB / SES$0.00All inside free tier at this volume
ACM certificate$0.00Free forever
Total≈ $0.80Domain registration ($12/yr) not included

CI/CD flow

  1. Push to main in the repo.
  2. GitHub Actions assumes an AWS IAM role via OIDC — zero long-lived credentials in the repo.
  3. Site change → npm ci → astro build → aws s3 sync → cloudfront invalidate /*.
  4. Infra change → terraform fmt → init → validate → plan on PR; apply on merge.
  5. Both workflows use path filters so a doc-only change doesn't trigger a full redeploy.

deliberate choices worth calling out

  • Terraform over CloudFormation. Same expressive power, better job market coverage, plan output is human-readable.
  • HTTP API v2 over REST API. Contact form doesn't need request validation plugins, API keys, or usage plans — the fancy stuff REST API gives you. HTTP v2 is the right default for a serverless contact endpoint.
  • Static Astro, not Next.js SSR. The site is 100% content. Every dynamic affordance (contact form) is a separate serverless endpoint. No point paying for warm Node servers on every request.
  • OAC, not signed URLs / OAI. Origin Access Control is the current AWS-recommended path — supports SigV4 and all S3 features, unlike legacy OAI.
  • Free-tier native, but not free-tier locked. Nothing here breaks if traffic 100×'s — CloudFront + S3 + Lambda all scale down to $0 idle and up to production-scale linearly. This exact template is what I'd use for a real product's MVP.

code

read the terraform →

The full IaC lives at github.com/dram64/dram-soc.org infra/ for Terraform, site/ for the Astro source, .github/workflows/ for the deploy pipeline.