In this guide, we'll build a simple deployment pipeline where a website stored in GitHub is automatically deployed to an Amazon S3 bucket whenever changes are pushed to the main branch.
No long-lived AWS access keys will be stored in GitHub.
🏗️ What is being built?
The idea is simple:
- Create a website.
- Store it in GitHub.
- Push the code.
- GitHub Actions starts automatically.
- GitHub authenticates with AWS using OIDC.
- AWS allows the workflow to assume an IAM role.
- The workflow uploads the website to S3.
- S3 serves the website.
The final flow looks like this:
GitHub
↓
GitHub Actions
↓
GitHub OIDC
↓
AWS IAM Role
↓
Amazon S3
↓
Static Website 🌐
📋 What is required?
Before starting, you'll need the following.
AWS
- An AWS account
- An S3 bucket
- An IAM role
- An IAM permissions policy
- GitHub OIDC configured for AWS
GitHub
- A GitHub repository
- A
mainbranch - GitHub Actions enabled
Local machine
- Git
- A text/code editor
- A simple
index.html
You don't need a server or an EC2 instance for this project.
🧰 What to prepare
For my project, I used the following setup:
AWS Region: <AWS_REGION>
S3 Bucket: <S3_BUCKET_NAME>
IAM Role: <IAM_ROLE_NAME>
GitHub Repository: <GITHUB_USERNAME>/<GITHUB_REPOSITORY>
Branch: main
For example, throughout this guide, replace:
<AWS_REGION>
<S3_BUCKET_NAME>
<IAM_ROLE_NAME>
<GITHUB_USERNAME>
<GITHUB_REPOSITORY>
<AWS_ACCOUNT_ID>
with your own values.
We'll prepare four main things:
- S3 bucket
- IAM role + permissions
- GitHub OIDC authentication
- GitHub Actions workflow
1️⃣ Prepare the S3 Bucket
Go to:
AWS Console → S3 → Create bucket
Create a bucket with a name such as:
my-aws-s3-project
💡 S3 bucket names must be globally unique. If this name is already taken, choose another one.
Choose your AWS region.
For example:
ap-southeast-1
Encryption
For server-side encryption, I used:
SSE-S3
This means S3 manages the encryption keys for the objects stored in the bucket.
Once the bucket is created, open it.
2️⃣ Prepare Static Website Hosting
Open:
S3 → Your bucket → Properties
Find:
Static website hosting
Enable it and set the index document to:
index.html
Save the configuration.
At this point, S3 knows that this bucket is going to be used to serve a website.
But we still need to put a website inside it.
3️⃣ Prepare the IAM Role
Next, we need an IAM role that GitHub Actions can use.
Go to:
AWS Console → IAM → Roles → Create role
Create a role such as:
Git-S3-Project
The important thing here is that we're creating a role, not creating an AWS access key and putting that key into GitHub.
The authentication flow will be:
GitHub
↓
OIDC token
↓
AWS STS
↓
IAM Role
↓
Temporary credentials
This means GitHub Actions can use temporary AWS credentials instead of storing long-lived AWS credentials.
4️⃣ Prepare the Trust Policy
The role needs to know:
Who is allowed to assume me?
That's what the trust policy controls.
For the GitHub repository, use a trust policy like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<AWS_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:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main"
]
}
}
}
]
}
Replace:
<AWS_ACCOUNT_ID>
<GITHUB_USERNAME>
<GITHUB_REPOSITORY>
with your own values.
For example, this part:
repo:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main
should point to the repository and branch that are allowed to assume the role.
Trust policy vs permissions policy
Keep this distinction in mind:
Trust Policy
↓
Who can assume the role?
Permissions Policy
↓
What can the role do?
We'll configure the permissions policy next.
5️⃣ Prepare the S3 Permissions
Now we need to tell AWS what the role is actually allowed to do.
For this project, the role needs permission to upload objects to the S3 bucket.
Use:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::<S3_BUCKET_NAME>/*"
]
}
]
}
Replace:
<S3_BUCKET_NAME>
with your bucket name.
The permission that matters most for our deployment is:
s3:PutObject
because GitHub Actions needs to upload the website files into S3.
💡 For a production setup, review these permissions carefully and follow the principle of least privilege. The example above is intended to keep this learning project straightforward.
6️⃣ Prepare the Website
Create an index.html in your GitHub repository.
For example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My AWS Website</title>
</head>
<body>
<h1>Hello from AWS S3 🚀</h1>
<p>This website is deployed using GitHub Actions.</p>
</body>
</html>
Your repository can now look like:
aws-s3-project/
│
├── index.html
│
└── .github/
└── workflows/
└── deploy.yml
7️⃣ Prepare GitHub Actions
Now we connect GitHub to AWS.
Create:
.github/workflows/deploy.yml
Add:
name: Deploy to S3 Bucket
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/<IAM_ROLE_NAME>
aws-region: <AWS_REGION>
- name: Upload files to S3
run: |
aws s3 cp ./index.html s3://<S3_BUCKET_NAME>/
Replace:
<AWS_ACCOUNT_ID>
<IAM_ROLE_NAME>
<AWS_REGION>
<S3_BUCKET_NAME>
with your own values.
There are three important parts here.
Trigger
on:
push:
branches:
- main
This means the workflow runs when code is pushed to main.
OIDC permission
permissions:
id-token: write
contents: read
id-token: write allows GitHub Actions to request an OIDC token.
It does not give GitHub access to S3.
AWS still decides whether that identity is allowed to assume the IAM role.
Upload
Finally:
aws s3 cp ./index.html s3://<S3_BUCKET_NAME>/
This copies the file into the S3 bucket.
8️⃣ Prepare Public Website Access
At this point, GitHub Actions can upload the file.
But that's a different question from:
Can someone visiting the website read the file?
For a basic S3 static website, the website objects need to be publicly readable.
For this learning project, you can use a bucket policy that allows public read access to index.html:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForWebsite",
"Principal": "*",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::<S3_BUCKET_NAME>/index.html"
]
}
]
}
Replace:
<S3_BUCKET_NAME>
with your bucket name.
Here:
"Principal": "*"
means anyone can be the requester, while:
"Action": "s3:GetObject"
allows them to read the object.
⚠️ This makes
index.htmlpublicly readable. This is appropriate for demonstrating a public static website, but you should not blindly use this access model for sensitive or private data.⚠️ This example only makes
index.htmlpublic. If your website contains CSS, JavaScript, images, or other files, you'll need to account for those objects too.
For production applications, a common approach is to keep the S3 bucket private and use Amazon CloudFront to serve the website.
✅ Once Done — How to Check
Now we have everything prepared.
Let's test it from end to end.
Check 1: Push the Code
Commit and push:
git add .
git commit -m "Deploy website to S3"
git push origin main
Check 2: Open GitHub Actions
Go to your GitHub repository:
Actions → Deploy to S3 Bucket
You should see a workflow run.
The steps should complete successfully:
✓ Checkout code
✓ Configure AWS credentials
✓ Upload files to S3
If these are green, GitHub successfully authenticated with AWS and uploaded the file.
Check 3: Check the S3 Bucket
Go to:
AWS Console → S3 → Your Bucket → Objects
You should see:
index.html
This confirms the file reached S3.
Check 4: Open the Website
Go back to:
S3 → Properties → Static website hosting
Copy the website endpoint shown there and open it in your browser.
You should see:
Hello from AWS S3 🚀
🎉 Your GitHub repository is now deploying a website to S3.
🔎 If Something Goes Wrong
Here are the main things to check.
GitHub Actions cannot assume the role
Check the IAM trust policy.
The GitHub repository and branch in the sub condition must match the workflow.
For example:
repo:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main
Also make sure the GitHub Actions workflow has:
permissions:
id-token: write
contents: read
GitHub assumes the role but S3 returns AccessDenied
Check the IAM permissions policy.
Make sure the role has:
s3:PutObject
for the correct bucket:
arn:aws:s3:::<S3_BUCKET_NAME>/*
Deployment succeeds but website shows AccessDenied
GitHub's deployment permission and visitor access are separate.
Check:
- S3 bucket policy
- Static website hosting
- Block Public Access settings
Presigned URL works but normal URL doesn't
A presigned URL provides temporary authorization to access an object.
It does not mean the object is publicly accessible.
For example:
Normal S3 URL
↓
Requires permission
Presigned URL
↓
Contains temporary authorization
🎯 Final Result
What we started with:
GitHub
What we ended with:
GitHub
↓
GitHub Actions
↓
OIDC
↓
IAM Role
↓
S3
↓
Static Website 🌐
And the deployment process is now automatic:
Change index.html
↓
git push
↓
GitHub Actions
↓
Upload to S3
↓
Website updated
The main takeaway is that GitHub Actions can authenticate with AWS using OIDC and assume an IAM role without storing long-lived AWS access keys in the repository.
That's the complete setup. 🚀
Top comments (0)