A reusable, self-bootstrapping infrastructure template for deploying modern React (Vite) static applications to AWS using:
- πͺ£ Amazon S3 (static hosting)
- π Amazon CloudFront (global CDN)
- π AWS IAM + GitHub OIDC (secure CI/CD authentication)
- βοΈ Terraform (infrastructure as code)
- β‘ GitHub Actions (multi-environment CI/CD: dev, staging, prod)
This project is designed as a drop-in frontend deployment foundation for any Vite + React application that needs scalable AWS hosting with automated deployments.
This system provisions and connects:
-
React + Vite App
- Built and deployed via GitHub Actions
-
S3 Bucket
- Stores built static assets
- Private bucket (no public access)
-
CloudFront Distribution
- Serves content globally
- Handles caching and HTTPS
-
IAM OIDC Role (GitHub Actions)
- Secure, keyless AWS authentication
- Least privilege access for deployment
-
Terraform Modules
- S3 static site module
- CloudFront module
- IAM OIDC module
- Environment-based configuration
.
βββ src/ # React (Vite) application source code
β
βββ public/ # Static assets served directly (faviconimages, etc.)
β
βββ index.html # Vite entry HTML file
βββ package.json + other configs # Project dependencies and scripts
β
βββ terraform/ # Infrastructure as Code (Terraform) directory
β β
β βββ bootstrap/ # One-time setup (state backend, foundational resource)
β β
β βββ modules/ # Reusable Terraform modules
β β β
β β βββ s3-static-site/ # S3 bucket + static hosting configuration
β β βββ cloudfront/ # CloudFront CDN distribution setup
β β βββ iam-oidc/ # GitHub Actions OIDC IAM role configuration
β β
β βββ environments/ # Environment-specific configurations
β β β
β β βββ dev/ # Development environment
β β βββ staging/ # Staging environment
β β βββ prod/ # Production environment
β
βββ .github/workflows/ # CI/CD pipelines (GitHub Actions)
β βββ deploy-dev.yml # Dev deployment workflow
β βββ deploy-staging.yml # Staging deployment workflow
β βββ deploy-prod.yml # Production deployment workflow
β
βββ README.md # Project documentation
This project was built with:
- Fully automated CI/CD pipeline (GitHub Actions)
- Secure AWS authentication using OIDC (no long-lived AWS keys)
- Environment-based deployments (dev / staging / prod)
- CloudFront invalidation on every deployment
- Reusable Terraform modules for multi-project usage
- Production-ready S3 security configuration
- Clean separation of infrastructure and frontend build
Before using this project, ensure you have:
- AWS Account
- Terraform β₯ 1.10+ (Required for S3 file lock feature introduced in v.1.10. enabling file lock in S3 allows us to lock our state file without the need for DynamoDB + S3 lock feature which is being deprecated by AWS)
- Node.js β₯ 20+
- Terminal for running commands (Bash recommended)
- GitHub repository
- AWS CLI configured (for local testing)
- Clone the repository
git clone https://github.com/ecoderP/s3-static-app-terraform.git
- Install Frontend Dependencies and build
After cloning the repository, install project dependencies, then run the npm build command. from the project root directory, run:
npm ci
npm run build
- There are preset customisable terraform variables in .tfvarsexample.
- Terraform state backend configurations are in .tfbackendexample files.
These are so named to bypass .gitignore. Gitgnore will ignore all .tfvars and .tfbackend files for security. You will need to rename .tfvarsexample and .tfbackendexample to .tfvars and .tfbackend extensions respectively.
For example, for terraform/bootstrap/ directory, update configuration settings, then:
cd terraform/bootstrap
mv terraform.tfvarsbackendexample terraform.tfvars
- In the terraform/bootstrap folder
- Personalise variables
- Initialise terraform
terraform init
Important: Copy the bucket name from terminal output. This is the shared backend state bucket name for all environments. Use this output as bucket name in .tfbackend for all environments.
- Configure environment
Each environment (dev/staging/prod) has its own configuration. Locate .tfbackend and .tfvars configuration files, personalise and rename for each environment. For example, for dev environment:
cd terraform/environments/dev
mv dev.tfbackendexample dev.tfbackend
terraform init -backend-config=dev.tfbackend
- Validate code and Deploy Infrastructure for each environment
terraform validate
terraform plan
terraform apply -auto-approve
This project uses GitHub Actions β AWS OIDC federation, meaning:
β No AWS access keys stored in GitHub
β Temporary credentials issued per workflow run
β Least-privilege IAM roles scoped per environment
GitHub Actions assumes a role like:
- Repository: Your-github-username/repo-name
- Branch-based conditions:
- dev β dev role
- staging β staging role
- main β production role
This project includes GitHub Actions workflows for:
- Trigger: push to dev
- Deploys to dev S3 bucket + CloudFront
- Trigger: push to staging
- Deploys to staging S3 bucket + CloudFront
- Used for pre-production validation
- Trigger: push to main
- Deploys stable build to production environment (S3 + CloudFront)
- Checkout code
- Install dependencies
- Build Vite React app
- Assume AWS role via OIDC
- Sync build to S3
- Invalidate CloudFront cache
To get your CI/CD pipeline working, add the following environment secrets to GitHub Actions:
- S3_BUCKET
- CLOUDFRONT_DISTRIBUTION_ID
- AWS_ROLE_ARN
To get the values for your secrets, from each environment directory (dev, staging, prod), run:
terraform output
This repo is designed as a starter backend infrastructure for any React + Vite frontend project.
module "frontend_hosting" {
source = "github.com/ecoderP/s3-static-app-terraform//modules/s3-static-site"
bucket_name = "my-new-app"
environment = "dev"
}
You can reuse this setup for:
- Portfolio sites
- SaaS frontend dashboards
- Admin panels
- Marketing landing pages
- Micro-frontends
Just change:
- bucket name
- CloudFront config
- environment variables
- S3 bucket is private by default
- CloudFront serves as the only public entry point
- IAM follows least privilege principle
- In in main.tf file inside terraform/bootstrap,
lifecycle {
prevent_destroy = false
}
is set to false for easy clean-up. In real production environments, I would set prevent_destroy as true. This protects the remote state bucket from accidental data loss.
In addition,
force_destroy = true
should be deleted or set as false in real production environments.
- From each environment directory (dev, staging and prod), run the command:
terraform destroy -auto-approve
Do this for all environments.
- From the terraform bootstrap directory, run the command:
terraform destroy -auto-approve
- Optional, but can do: List all AWS buckets in your account and confirmbuckets are not listed. Run the command:
aws s3 ls
Breaking infrastructure into reusable Terraform modules made the project significantly easier to maintain and scale across environments.
Separating:
- S3 configuration
- CloudFront setup
- IAM/OIDC authentication
allowed infrastructure changes to be isolated without affecting the entire stack.
Using GitHub OIDC federation eliminated the need to store AWS access keys in GitHub Secrets.
This project provided hands-on experience with:
- IAM trust policies
- federated authentication
- least-privilege access design
and highlighted modern cloud security best practices.
Separating dev, staging, and production infrastructure reduced accidental cross-environment changes and improved deployment confidence.
This also made testing infrastructure changes safer before promoting them to production.
Managing Terraform state becomes increasingly important as infrastructure grows.
This project reinforced:
- the importance of remote state backends
- state locking
- consistent environment structure
- predictable resource naming conventions
A deployment pipeline should be treated as part of the infrastructure rather than an afterthought.
Automating:
- builds
- deployments
- authentication
- CloudFront invalidations
improved reliability and reduced manual deployment errors.
Minor IAM or bucket policy mistakes can completely break deployments.
Troubleshooting issues such as:
- AccessDenied errors
- incorrect OIDC trust relationships
- CloudFront origin permissions
- S3 bucket policy conflicts
helped build deeper AWS troubleshooting skills.
Making a project reusable is not automatic.
It required:
- parameterized Terraform variables
- environment abstraction
- clean module boundaries
- predictable naming conventions
This project reinforced the importance of designing for reuse from the beginning rather than trying to fix it later.
A working deployment is not necessarily production-ready.
This project highlighted the balance between:
- security
- scalability
- automation
- maintainability
- developer experience
when building real-world cloud infrastructure.
Clear documentation became essential as the project grew in complexity.
Writing reusable setup instructions and architecture explanations can improve:
- onboarding
- maintainability
- troubleshooting
- long-term project usability
- Add custom domain + Route53 automation
- ACM SSL certificate provisioning
- Integrate terraform into CI/CD pipeline
- Automated performance testing in CI
Built and Maintained by ecoderP