All cloud projects
LIVE us-east-1 2026
CLOUD PROJECT · 2026

andruxa.dev

Static portfolio hosted on AWS with Infrastructure as Code. Terraform-managed S3 + CloudFront + Route 53, with automated deploys via GitHub Actions OIDC — no long-lived credentials.

S3CloudFrontRoute 53ACMTerraformGitHub Actions

The constraint

A portfolio site for a cloud-infrastructure role should itself be the artifact. That means the stack has to be defensible on its own terms — not because it’s clever, but because the trade-offs are right for the problem. The problem here is small: a handful of static pages, single author, no forms, no auth, no database, single-digit-RPS traffic at best. That sizing rules out a server. It rules in content-addressable storage behind a CDN, which is the boring, reliable, sub-$2-per-month answer. Total run-rate sits at roughly $18 per year, and the line item that dominates is Route 53’s hosted zone plus the domain registration — everything else is fractional cents.

The architecture

Four AWS services do the work. S3 holds the built artifacts in a private bucket — no website-hosting mode, no public ACLs, no bucket-policy hack. CloudFront sits in front with an Origin Access Control that signs each origin request with SigV4, so the bucket only answers to this one distribution. A Response Headers Policy attached to the default cache behavior emits HSTS preload, a strict default-src 'self' CSP, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, and a Permissions-Policy that disables camera/microphone/geolocation/interest-cohort by default. Route 53 holds the apex zone with alias records pointing at the distribution. The ACM certificate lives in us-east-1 — not a regional preference; CloudFront edges only accept certificates from that region.

BrowserRoute 53CloudFrontS3 (private, OAC)DNSaliassigv4Response Headers Policygit push mainGitHub ActionsIAM RoleOIDCs3 sync + invalidate
Read path (top): browser → Route 53 → CloudFront (with Response Headers Policy) → private S3 bucket via OAC. Deploy path (bottom): push to main → GitHub Actions → OIDC → scoped IAM role → S3 sync + CloudFront invalidation.

The deploy pipeline

The differentiator isn’t the static stack — it’s that no long-lived AWS credentials live anywhere in GitHub. Pushes to main trigger a workflow that exchanges GitHub’s OIDC token for short-lived AWS credentials via sts:AssumeRoleWithWebIdentity. The trust policy on the deploy role pins to repo:andruxap/resume-website:ref:refs/heads/main and the production environment, so only the right repo, the right branch, and a workflow run inside that GitHub environment can assume it. Permissions on the role are scoped to one S3 bucket and one CloudFront distribution — nothing else in the account is reachable, even if the role is misused.

The sync itself is two passes by design. Hashed assets under _astro/ get Cache-Control: public, max-age=31536000, immutable — they never need revalidation because the filename changes when the bytes change. HTML, the sitemap, and robots.txt get Cache-Control: public, max-age=300, must-revalidate — they’re the routes whose meaning changes between deploys. After the sync, the workflow creates a CloudFront invalidation over /* and waits for completion before running curl smoke tests against /, /about/, and a known-404 path. End-to-end, push-to-live takes about three minutes.

What I’d do differently at scale

For a higher-traffic or multi-author version of this stack, three changes earn their keep. Multi-region origin failover matters once the cost of an S3 outage exceeds the cost of cross-region replication and the slightly fussier IaC — CloudFront is already global, but the origin isn’t. WAF earns its base fee the moment there’s anything to protect: a login surface, an authenticated API, a form-driven workflow. And build-time content sourced from a headless CMS (Contentful, Sanity, Astro DB) decouples editing from the git workflow — useful when the people writing copy aren’t the people writing infrastructure.

I’d also automate the infrastructure deploy itself. Right now terraform apply runs from my laptop; for a team or for a more defensible change history, that should be a workflow that opens a PR with terraform plan as a comment, gates the apply behind environment-protected reviewers, and runs from inside an OIDC-authenticated session — same pattern as the site deploy, applied to the stack underneath it.