index

Blog infrastructure

Following on from The road to a blog, here’s the technical detail of how this site is actually put together.

AWS Hosting

I went with AWS mainly to build my skills including the use of Terraform (which is not specific to AWS). I figured the domain could wait until the blog itself was in decent shape, but ended up buying one anyway: asebi.dev, via Route 53, for a year. That’s what Terraform uses to serve the blog.

There’s no staging environment in any meaningful sense. If I want to test something before it goes live, that means working locally and not pushing to main. Fine — just a workflow to get used to.

Using Terraform for S3, Route 53 and CloudFront

Terraform lets you define infrastructure as code and apply it to a cloud provider. For this blog, it’s managing:

  • S3 — the bucket that stores the site files, permissions so CloudFront can read from it, and a policy blocking any public access to the bucket directly.
  • ACM Certificate — an SSL/TLS certificate from Amazon so the blog runs over HTTPS.
  • Route 53 — a hosted zone for the domain, plus alias records for both asebi.dev and www.asebi.dev pointing at the CloudFront distribution. (The domain itself was registered separately in the AWS console — Terraform doesn’t handle that part.)
  • CloudFront — the Content Distribution Network. Serves content from the S3 bucket, cached at locations near whoever’s requesting it. It also handles a few things that make Astro work properly on S3: a URI rewrite function that maps clean URLs to their index.html files, security headers (HSTS, CSP, etc.), and a separate long-lived cache for the content-hashed /_astro/ assets.
  • IAM Policies — a dedicated deploy user with a scoped policy: S3 sync permissions and CloudFront cache invalidation only. Terraform outputs the access key, which goes into GitHub as secrets for CI.

The actual workflow:

  1. Log in to AWS.
  2. Run terraform init — sets up the working directory and pulls providers.
  3. Configure the files.
  4. Run terraform plan — shows what would change. First run threw No valid credential sources found, fixed by running aws configure with a fresh access key.
  5. Run terraform apply — creates or updates the infrastructure. This stands up the S3 bucket and CloudFront distribution.

One note: I merged a Dependabot update to a Terraform provider version, and Terraform didn’t pick it up on its own. Re-running terraform init pulls the new version.

Astro URI Rewrite Function

The URI rewrite function is worth a quick note. S3 serves files — it doesn’t know that /about should resolve to /about/index.html. Without a fix, any clean URL that isn’t the root will 404. The CloudFront Function runs on every request at the edge and rewrites the URI before it hits S3:

function handler(event) {
  var request = event.request;
  var uri = request.uri;
  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  } else if (!uri.includes('.')) {
    request.uri += '/index.html';
  }
  return request;
}

A trailing slash gets index.html appended; anything without a file extension (i.e. a clean URL) gets /index.html appended. Paths with extensions — CSS, images, JS — pass through untouched.

GitHub Actions

Setting up CI/CD was one of the reasons for doing this at all. Every push to main triggers a workflow with three jobs: secret scan, validate & build, and deploy to S3.

Secret Scan

Uses gitleaks to scan for secrets — passwords, API keys, tokens. For a blog, the main risk is accidentally posting something from my lab environment, or infrastructure secrets that ended up in the Terraform code. If gitleaks flags something, I either edit the content, or, if it’s an infra file, add it to .gitignore so it’s never tracked in the first place.

Validate & Build

Uses pnpm to lint, check formatting, validate all published posts (frontmatter and word count), and build the Astro site. It also runs smoke tests to catch any obvious breakage. The build output is uploaded as an artifact and passed to the deploy job.

Deploy to S3

With AWS credentials stored as GitHub secrets, this syncs the build result to the S3 bucket and invalidates the CloudFront cache. Every push to main goes live automatically, no other intervention needed.

Whether any of this was necessary and simpler hosting options

For a static blog, this is a lot of infrastructure but I made these choices for learning reasons in addition to creating something for public viewing. For comparison, I could have gone with any of the below for arguably less effort.

  • S3 + CloudFront without Terraform gets the same result, but the setup only exists in the console. No record of what’s deployed, harder to recreate or tear down, and prone to drift if I go in and click around. Terraform’s value here isn’t the static site itself, it’s that the infrastructure is reproducible and inspectable. This aligns closely with prior experiences using Ansible, and now using NixOS as my main Linux OS.
  • GitHub Pages — free, no infrastructure, custom domain with TLS included. I’d still write my own Actions workflow for the secret scan and build steps I already have, and I’d get less control over caching and headers than CloudFront.
  • Netlify — closest like-for-like. Push to deploy, no Actions workflow needed at all. Branch and PR deploys give free preview environments, which solves the staging gap directly.

The honest version: I picked AWS specifically to build AWS and Terraform skills, not because it was the right tool for hosting a blog. That’s a fine trade, but it’s “more to learn and maintain” against Netlify’s “less to learn, more convenience, and staging for free.” Cost-wise this setup runs to a dollar or two a month for the Route 53 zone and minimal S3/CloudFront usage, plus domain registration. Netlify or Pages would be free. If you’re reading this and thinking about doing the same, this should help steer you towards a preferred direction.

Emacs Integration

Lastly, I also have two Emacs functions that I trigger with a shortcut that exports a post directly to the website repo.

  • Export (SPC B e) — converts the current org buffer to Astro-flavoured markdown and writes it to src/content/posts/. Images are copied across to _assets/ automatically. Set #+BLOG_DRAFT: t to export as a draft (prefixes the filename with _ so it won’t publish).
  • Review (SPC B c / SPC B l / SPC B r) — three modes: static template checks (word count, intro length, empty sections), async LLM review via a local ollama model, or both together.

One that I need to remember is that after exporting, I have to manually commit and then push to GitHub. That’s what kicks off the CI workflow and updates the live site. This is pretty straightforward as I’m already in Emacs and it just takes a couple basic steps and gives me a chance to check for errors or fix links.