The stack#
infra/ is an OpenTofu declaration of everything the site runs on:
| Resource | Why |
|---|---|
| S3 bucket (private, versioned) | Origin for the exported bundle. No public access at all. |
| CloudFront + Origin Access Control | Reads the bucket over SigV4, terminates TLS, compresses. |
| CloudFront Function | Rewrites /docs/deploy/ → /docs/deploy/index.html at the edge. |
| ACM certificate (us-east-1) | CloudFront viewer certificates must live in us-east-1. |
| Response-headers policy | CSP, HSTS, X-Frame-Options: DENY, referrer policy. |
| Route53 ALIAS A/AAAA | Optional — only when route53_zone_id is set. |
| IAM OIDC provider + deploy role | Lets GitHub Actions publish without any long-lived key. |
tofu -chdir=infra fmt -check -recursive tofu -chdir=infra init -backend=false tofu -chdir=infra validate
First apply#
cp infra/terraform.tfvars.example infra/terraform.tfvarsand fill it in.tofu -chdir=infra init && tofu -chdir=infra plan.- Apply. If the hosted zone is external, take the records from the
certificate_validation_recordsoutput and create them; the apply waits on validation. - Read the outputs — they are exactly the repository variables the workflow needs.
Repository variables#
Set these under Settings → Secrets and variables → Actions → Variables. None is a secret — the role ARN is useless without a token from the trusted repo and ref.
| Variable | Source |
|---|---|
AWS_DEPLOY_ROLE_ARN | tofu output -raw deploy_role_arn |
AWS_REGION | sa-east-1 |
S3_BUCKET | tofu output -raw bucket_name |
CLOUDFRONT_DISTRIBUTION_ID | tofu output -raw distribution_id |
SITE_URL | tofu output -raw site_url |
The pipeline#
ci.yml runs on every pull request: validate:content (the Markdoc schema gate — the plugin does not validate on its own), typecheck, lint, the static build, and tofu fmt/validate.
deploy.yml runs on a push to main. It assumes the deploy role via OIDC, then:
- Two sync passes. Fingerprinted assets under
_next/staticgetmax-age=31536000, immutable; HTML getsmax-age=0, must-revalidate. OneCache-Controlcannot be correct for both — with a single immutable pass, readers keep seeing the old page long after the invalidation finished. --deleteon the HTML pass only, so a removed page actually disappears. 3. Invalidate and wait for the invalidation to complete. 4. Verify the edge serves it — a200from the home page and from a deep link. A deploy that reports success without asking the edge for the artifact has proved nothing about what readers see.
The deploy-production concurrency group is cancel-in-progress: false on purpose: cancelling a sync --delete mid-flight leaves the bucket holding half of two bundles.
Deploying by hand#
scripts/deploy.sh performs the identical sequence when CI cannot run:
S3_BUCKET=$(tofu -chdir=infra output -raw bucket_name) \ CLOUDFRONT_DISTRIBUTION_ID=$(tofu -chdir=infra output -raw distribution_id) \ SITE_URL=$(tofu -chdir=infra output -raw site_url) \ pnpm deploy