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.
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_IDandAWS_SECRET_ACCESS_KEYin 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:
git push origin main
↓
GitHub Actions
↓
Build Astro (SSG)
↓
Sync dist/ → S3
↓
Invalidate CloudFront
↓
Updated websiteIf 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:
{
"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):
{
"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
AWS_ROLE_ARN
BLOG_DISTRIBUTION_IDVariables (non-sensitive)
AWS_REGION
BLOG_BUCKET_S3No access keys stored. No permanent secrets hanging around.
Step 3 — GitHub Actions workflow
Create the file:
.github/workflows/deploy.ymlFull content:
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:
"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:
- Pick a stack with common sense
- Prepare Astro without hacks
- Serve HTML via S3 + CloudFront
- Custom domain and Google ready
- 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↗