All posts

Rebuilding this site, and why its first deploy failed

A static site where every layer does a real job, for about a dollar a month, and the one-line mismatch that kept GitHub out of AWS.

The previous version of this site did a lot: a chat, a shared XR space, a blockchain, payments. It was fun to build and a lot to keep running. For a portfolio, most of it was weight.

So I started again with a few rules: the site should be fast and accessible, cost very little to run (about a dollar a month), and every layer of the stack should do a real job, nothing there just to be on a list.

Static first

The site is built with Astro and strict TypeScript, and every page is rendered to plain HTML ahead of time. The summit ridge on the home page is drawn from the real elevations of the Seven Summits when the site builds, not in the visitor’s browser. There are only a few small scripts: the theme toggle, the local time in Austin, and the like. There are no analytics, cookies or trackers.

Even the link preview and the icons are generated at build time, from the same fonts and the same ridge as the page, so they can’t drift out of date when the page changes.

Checks before anything ships

One command runs everything: formatting, linting, type checks, unit tests, and end-to-end tests in a real browser, including automated accessibility checks against the testable WCAG 2.2 AA criteria on every page, in light and dark mode, on desktop and phone sizes. (Automation catches part of WCAG, not all of it; the rest is manual review.) GitHub Actions runs the same checks on every push, and nothing deploys unless they pass.

Shipping without keys

The site lives in a private S3 bucket that only CloudFront can read. CloudFront sits in front as the public edge: it serves the files from locations around the world over HTTPS, while the bucket itself stays unreachable from the public internet. To deploy, GitHub Actions needs to write to that bucket, and the usual shortcut is to paste a long-lived AWS access key into GitHub’s secrets. Keys like that leak.

Instead, AWS trusts GitHub’s own identity tokens (OpenID Connect). When a deploy runs, GitHub issues a signed token saying which repository and branch it’s running for. AWS STS validates that token against the IAM role’s trust policy (essentially the rule defining which GitHub workflows may assume the role) and, if it matches, returns short-lived credentials that expire within the hour. The role’s permissions allow exactly three things: list the bucket, upload to it, and clear CloudFront’s cache.

Why the first deploy failed

The checks passed, the build passed, and then:

Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity

My trust policy said: allow tokens whose subject (sub claim) is

repo:minamarin/marinminamiya.com:ref:refs/heads/main

That’s the format in most examples, and it’s the shape I expected. My first guess was that the deploy job’s environment: setting was the problem, because using a GitHub environment changes the subject to name the environment instead of the branch. I removed it, pushed again, and got the same error.

So I stopped guessing and asked GitHub’s API how this repository’s OIDC subject is actually configured. The answer: use_immutable_subject: true. In 2026 GitHub changed the default subject claim: repositories created after 15 July 2026 use an immutable subject, which names the owner and the repository by their permanent numeric IDs as well as their names. This repository was created in September, so it got the new format automatically:

repo:minamarin@120408542/marinminamiya.com@1388201825:ref:refs/heads/main

Many examples online still show the old format. One string, two extra IDs, and my trust policy never matched. After I updated it, the next deploy took 19 seconds.

The fix ended up stricter than what I’d started with, which is the point of GitHub’s change. A policy based on names would trust anyone who later took over a repository or account with the same name; IDs can’t be reused like that.

The lesson is an old one, and I’m writing it down so I remember it: when two systems disagree, check what one is actually configured to send the other before changing anything else.

What it costs

At this site’s current traffic, S3 and CloudFront come to about a dollar a month, with a budget alert at five. The domain and email stay where they are.