How I Built a Static Website on AWS S3 with Terraform and GitHub Actions

π Why I Built It
I wanted to host a simple static website entirely on AWS, using infrastructure as code and automating deployment with GitHub Actions. This approach lets me manage the infrastructure reliably and deploy updates seamlessly. In this article, Iβll show you how I used Terraform to create an S3-backed static website, configured the bucket, and automated deployments via GitHub Actions.
π What I Used
AWS S3: to host HTML/CSS files
Terraform: to automate deployment
PowerShell/CLI: for uploading files to S3
π§± Step-by-Step Setup
Update
terraform.tfvars
with your desired bucket name:bucket_name = "your.unique.bucket.name"
Initialize Terraform:
terraform init
Review the execution plan:
terraform plan
Apply the changes:
terraform apply
After successful apply, Terraform outputs the website URL:
website_url = http://your.unique.bucket.name.s3-website-<region>.amazonaws.com
Open the URL in your browser to see your static website.
(Optional) Set up GitHub Secrets in your repository for AWS credentials and region:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION
Push your changes to the
main
branch to trigger GitHub Actions and deploy your site automatically.
GitHub Actions Workflow
The .github/workflows/deploy.yml
file uses GitHub Actions to sync your site-content/
directory to the S3 bucket on every push to the main
branch. It uses AWS credentials stored in GitHub Secrets and does not apply ACLs since the bucket policy handles permissions.
name: Deploy Static Website to S3
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Sync website content to S3
run: aws s3 sync site-content/ s3://your.unique.bucket.name --delete
- name: Verify deployment
run: aws sts get-caller-identity
Project Structure
terraform-s3-static-site/
βββ main.tf # S3 bucket and website config resources
βββ variables.tf # Variable definitions
βββ outputs.tf # Outputs the website URL
βββ terraform.tfvars # Bucket name and other variable values
βββ site-content/ # Static website files (index.html, error.html, etc.)
βββ .github/
β βββ workflows/
β βββ deploy.yml # GitHub Actions workflow for deployment
Notes
Ensure the bucket name is globally unique.
The URL generated will correspond directly to the specified bucket name.
The project sets
force_destroy = true
on the bucket, so the URL, bucket, and its contents will be deleted if you runterraform destroy
.Do not use
--acl public-read
withaws s3 sync
if your bucket has ACLs disabled (best practice).Use GitHub Secrets to securely store AWS credentials for CI/CD.
π (Optional) Add Custom Domain + HTTPS
If using Route 53:
Create a CloudFront distribution
Use AWS Certificate Manager (ACM) for SSL
Point your domain's DNS to CloudFront
(I'll cover this in a separate post.)
π§© Lessons Learned
S3 makes static site hosting dead simple
Don't forget the public read policy
HTTPS requires CloudFront and ACM
π Whatβs Next
Add HTTPS and WAF via CloudFront
Integrate a contact form using Lambda + API Gateway
Fully automate with Terraform
Source Code
You can find the full Terraform project and GitHub Actions workflow on my GitHub repository.
π¬ Feedback?
If you're doing something similar or have questions, feel free to reach out or drop a comment.
License
MIT License
Subscribe to my newsletter
Read articles from Mubarak Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
