Back to articles
10 min readastro-ssg-aws-s3-cloudfront

Written by Rodrigo de Miguel

Automate Astro SSG blog deployment with GitHub Actions and AWS (5/5)

How to automate the deployment of a static Astro blog with SSG on AWS using GitHub Actions and OIDC, cutting manual steps and keeping CI simple, secure, and easy to live with.

GitHub Actions
AWS
CI/CD
Static Site Generation SSG
Infrastructure

The last manual step always ends up being annoying

At this stage of the series, the heavy lifting is already done and everything important is up and running:

  • An Astro blog
  • Static HTML (SSG)
  • S3 + CloudFront serving content fast and cheap
  • Custom domain with HTTPS
  • Google Search Console properly wired

Yet there’s one small detail that looks harmless at first and that, over time, starts to wear you down:

👉 deployment is still manual.

At the beginning it’s fine. Then one day you forget. Later you ask yourself: “wait, is this already in production?”. And after a while, even the smallest tweak feels more expensive than it should.

The answer is obvious: automate deployment. Just don’t turn a simple blog into a CI monster.

That’s exactly what this post covers.


What we're going to do (and what we're not)

We will do

  • Use GitHub Actions
  • Automate build + deploy + cache invalidation
  • Get rid of long-lived AWS credentials
  • Work with OIDC, the standard approach today

We will NOT do

  • Overcomplicated pipelines
  • Terraform
  • Multiple environments
  • Enterprise-level overengineering

This is still a personal portfolio / blog, not a corporate platform.


GitHub Actions + AWS OIDC

There’s a key decision here that’s worth stopping on for a moment.

We could:

  • store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in GitHub
  • call it done and move on

But that approach:

  • doesn’t age well
  • goes against current AWS guidance
  • and misses the point if you care about doing things properly

The sensible path today is clear: 👉 OIDC (OpenID Connect)

Why it makes sense:

  • No permanent keys stored anywhere
  • Access limited to a specific repo and branch
  • Temporary credentials issued on demand
  • Fully aligned with what AWS and GitHub recommend

Final deployment architecture

The full flow looks like this:

TEXT
git push origin main GitHub Actions Build Astro (SSG) Sync dist/ → S3 Invalidate CloudFront Updated website

If the build breaks: 👉 nothing gets deployed

That single guarantee already buys a lot of calm.


Step 1 — Configure OIDC between GitHub and AWS

1.1 Create Identity Provider in AWS

In AWS Console → IAM → Identity providers:

  • Provider type: OpenID Connect
  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Save.

With this in place, GitHub can identify itself to AWS in a controlled and verifiable way.

1.2 Minimal permissions policy

Create a policy that grants access only to the S3 bucket and CloudFront distribution you need:

JSON
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"], "Resource": [ "arn:aws:s3:::<YOUR_BUCKET>", "arn:aws:s3:::<YOUR_BUCKET>/*" ] }, { "Effect": "Allow", "Action": "cloudfront:CreateInvalidation", "Resource": [ "arn:aws:cloudfront::<YOUR_ACCOUNT_ID>:distribution/<YOUR_DISTRIBUTION_ID>" ] } ] }

Give it a clear name and save it. For example: policy-access-s3-bucket-demo-astro-blog-s3

That’s all you need. Nothing more than strictly required.

1.3 Create an IAM Role for GitHub Actions

In IAM → Roles → Create role:

  • Type: Web identity
  • Identity provider: token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com
  • GitHub Organization: your org or username
  • GitHub Repository: your repo
  • GitHub Branch: main (or whichever branch you deploy from)

Trust Policy (the important bit)

Edit the trust policy and set it like this (adjust org/repo values):

JSON
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { "token.actions.githubusercontent.com:sub": "repo:<YOUR_GITHUB_ORG>/<YOUR_GITHUB_REPO>:ref:refs/heads/main" } } } ] }

This keeps access locked down to the minimum:

  • that repository
  • that branch (main)

Save the role ARN, you’ll need it in the next step.

Done.


Step 2 — Variables and secrets in GitHub

In the repo → Settings → Secrets and variables → Actions

Secrets

TEXT
AWS_ROLE_ARN BLOG_DISTRIBUTION_ID

Variables (non-sensitive)

TEXT
AWS_REGION BLOG_BUCKET_S3

No access keys stored. No permanent secrets hanging around.


Step 3 — GitHub Actions workflow

Create the file:

TEXT
.github/workflows/deploy.yml

Full content:

YAML
name: Deploy Astro SSG to S3 + CloudFront on: push: branches: - main workflow_dispatch: # Enables the "Run workflow" button permissions: id-token: write contents: read jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci - name: Build Astro site run: npm run build # 👇 Only runs if the build succeeds - name: Configure AWS credentials via OIDC uses: aws-actions/configure-aws-credentials@v5.1.1 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ${{ vars.AWS_REGION }} - name: Deploy to S3 run: | aws s3 sync dist/ s3://${{ vars.BLOG_BUCKET_S3 }} \ --delete \ --region ${{ vars.AWS_REGION }} \ --cache-control max-age=31536000 - name: Invalidate CloudFront cache run: | aws cloudfront create-invalidation \ --distribution-id ${{ secrets.BLOG_DISTRIBUTION_ID }} \ --paths '/*'

Important details (real experience)

npm ci beats npm install

  • Faster
  • Predictable
  • Uses exactly what’s locked in package-lock.json

Pin the Node version

Match the version you use locally.

In package.json:

JSON
"engines": { "node": "22.13.1", "npm": "10.9.2" }

That alone avoids a surprising amount of CI weirdness.

Done.


What we've gained with this

  • ✅ No manual steps left
  • ✅ Deployment blocked if the build fails
  • ✅ Modern security with OIDC
  • ✅ Straightforward, low-maintenance setup

And the best part: 👉 deployment disappears from your mental load


Final checklist

  • Push to main
  • GitHub Actions runs
  • Build passes
  • S3 syncs
  • CloudFront invalidates
  • Website updated

If something goes wrong, the logs make it obvious.


Series wrap-up

With this post, the loop is closed:

  1. Pick a stack with common sense
  2. Prepare Astro without hacks
  3. Serve HTML via S3 + CloudFront
  4. Custom domain and Google ready
  5. Minimal, automatic CI

It’s not the fanciest infrastructure out there. It’s simple, predictable, and it stays out of the way while doing its job.

Cheers, devs.


Posts in this series

1️⃣ Part 1: A blog shouldn't be a SaaS
2️⃣ Part 2: Preparing an Astro blog with good judgment
3️⃣ Part 3: S3 + CloudFront to serve a fast and cheap static blog
4️⃣ Part 4: Custom domain with Route 53 and CloudFront for an Astro SSG blog
👉 Part 5: Automate Astro SSG blog deployment with GitHub Actions and AWS
🐙 GitHub Repo: demo-astro-ssg-s3-cloudfront

Let's talk?

Looking for someone who understands product as much as code?

Start a conversation

© 2026 Rodrigo de Miguel. All rights reserved.