Overview
A written series on deploying an Astro site to Amazon S3 behind CloudFront from GitHub Actions, without long-lived AWS access keys: OIDC, IAM, a private bucket, clean URLs and the failures you will hit.
I wanted to build a personal engineering blog where I could document projects, experiments, embedded-systems development, FPGA work, Linux troubleshooting, software projects, and everything else I learn along the way.
But I didn’t want publishing an article to involve manually building the website, uploading files to a server, or logging into AWS every time.
I wanted the deployment process to be completely automated.
The goal was:
The final architecture uses:
- Astro
- GitHub
- GitHub Actions
- GitHub OpenID Connect (OIDC)
- AWS IAM
- Amazon S3
- Amazon CloudFront
- CloudFront Functions
- A custom domain
The most important security decision was that I did not store a permanent AWS access key and secret key in GitHub.
Instead, GitHub Actions authenticates to AWS using OIDC and assumes a restricted IAM role.
The goal
The requirements were straightforward:
- Build the blog with Astro.
- Keep all source code in GitHub.
- Automatically deploy after pushing to
main. - Build the Astro project inside GitHub Actions.
- Upload only the generated
dist/directory to S3. - Serve the website through CloudFront.
- Keep the S3 bucket private.
- Use a custom domain.
- Support clean URLs such as:
https://example.com/projects
instead of:
https://example.com/projects/index.html
- Avoid storing permanent AWS credentials in GitHub.
The final architecture looks like this:
What this series covers
- Building the Astro site in GitHub Actions: Why Astro fits a static engineering blog, what the repository must contain for CI to work, and why the workflow installs dependencies with npm ci.
- Creating a private S3 bucket: Creating the S3 bucket that holds the generated website and keeping it private so only CloudFront can read it.
- Authenticating GitHub Actions to AWS with OIDC: Creating the GitHub OIDC identity provider in AWS and an IAM role restricted to this repository and branch, so the workflow assumes a role instead of storing access keys.
- Configuring the workflow: secrets and the role ARN: Why the configuration is stored as GitHub secrets rather than variables, and what the final deploy.yml looks like.
- Building, uploading to S3 and serving through CloudFront: The build and upload steps, why CloudFront sits in front of S3, and using a CloudFront Function to serve clean URLs without index.html.
- Publishing an article and keeping configuration clean: What happens after an article is written, the repository structure, and why secrets and variables are not hard-coded into the workflow.
- Troubleshooting and the production workflow: The failures that actually happen in CI: lock file errors, OIDC AssumeRoleWithWebIdentity errors, S3 access denied and region mismatches, plus how to add cache invalidation.
Each part is a self-contained write-up, so read them in order or jump straight to the one you need.