About this site
This site is also an infrastructure project. It is provisioned and shipped with the same practices I use at work: everything in code, changes reviewed through pull requests and a pipeline that does the rest.
Architecture
Visitor ──HTTPS──▶ Route 53 ──▶ CloudFront (CDN + TLS/ACM)
│ OAC (private access)
▼
S3 (private bucket)
GitHub ──push──▶ GitHub Actions ──OIDC──▶ AWS
├─ lint + scan → plan (PR) / apply (main)
└─ Astro build → s3 sync → invalidation
(deploy only after apply)The pages are static HTML generated by Astro. Route 53 alias records for the apex and www point to a CloudFront distribution, which terminates TLS (1.2+, HTTP/2 and HTTP/3) with an ACM certificate validated via DNS. A CloudFront Function redirects www to the apex and rewrites /page/ to/page/index.html. Content comes from a private S3 bucket that only CloudFront can read, through Origin Access Control. Every response carries security headers (HSTS, CSP, X-Frame-Options, Referrer-Policy) from a response headers policy.
Infrastructure as code
The infrastructure is split into two Terraform stacks. bootstrap is the only manual step and runs once: it creates the remote-state S3 bucket (versioned, encrypted, with native S3 locking), the GitHub OIDC provider, least-privilege IAM roles and even the GitHub repository variables, through the GitHub provider. The role trust policies are pinned to the repository's immutable OIDC subject (owner and repository numeric IDs). main creates the site bucket, the CloudFront distribution, the ACM certificate, the DNS records, an AWS Budget alert and the SSM parameters consumed by the pipeline.
Pipeline
A single GitHub Actions workflow detects whether a change touches infrastructure, the site or both. Infrastructure changes run terraform fmt, validate, tflint and checkov; on pull requests the Terraform plan is posted as a comment, and on main it is applied. The site is deployed only after the apply succeeds (or right away when infra did not change): Astro build, sync to S3 with a long immutable cache for hashed assets and HTML that is always revalidated, then a CloudFront invalidation. The deploy job reads the bucket name and distribution ID from SSM Parameter Store, so even the first deploy needs no copy and paste. GitHub authenticates to AWS over OIDC, with no access keys stored in GitHub, and each job has its own role: the deploy role can only write to the bucket and invalidate the cache.
Costs
Estimated: about US$15/year for the .com domain, plus US$0.50/month for the Route 53 hosted zone. S3, CloudFront, ACM and SSM stay at roughly zero at portfolio traffic levels. An AWS Budget emails me at 80% of US$5/month (actual) and at 100% (forecasted).
Decisions and next steps
A static site has no servers to patch and almost nothing to pay for, and it is the right fit for content that changes a few times a month. Astro generates plain HTML with no client-side JavaScript by default. Possible next steps: Lighthouse checks in CI, CloudFront access logs and AWS WAF in front of the distribution.