diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index fe9fedd..cffd5fa 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -1,4 +1,4 @@ -name: Deploy Dev +name: Deploy to Development on: push: @@ -9,6 +9,13 @@ permissions: id-token: write contents: read +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +concurrency: + group: development-deployment + cancel-in-progress: true + jobs: deploy: runs-on: ubuntu-latest @@ -17,30 +24,43 @@ jobs: defaults: run: - working-directory: vite-app + working-directory: . steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v5 with: node-version: 22 + cache: npm + cache-dependency-path: package-lock.json - - run: npm install + - name: Install dependencies and build + run: npm ci - - run: npm run test -- --run + - name: Build application + run: npm run build - - run: npm run build + - name: Debug GitHub Context + run: | + echo "Repository: ${{ github.repository }}" + echo "Ref: ${{ github.ref }}" + echo "Environment: dev" - - uses: aws-actions/configure-aws-credentials@v4 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1 - - run: | + - name: Sync files to S3 + run: | aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }} --delete - - run: | + - name: Invalidate CloudFront distribution + run: | aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \ --paths "/*" diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 11e0059..d54f0ae 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -38,12 +38,6 @@ jobs: - name: Build application run: npm run build - - name: Debug GitHub Context - run: | - echo "Repository: ${{ github.repository }}" - echo "Ref: ${{ github.ref }}" - echo "Environment: prod" - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: diff --git a/.gitignore b/.gitignore index dc93deb..5c836a9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data *.tfvars *.tfvars.json -.tfvars* +.tfvars # Ignore override files override.tf diff --git a/README.md b/README.md index b24dda6..1010aed 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,415 @@ -terraform init -migrate-state -backend-config=dev.tfbackend -terraform init -reconfigure -backend-config=dev.tfbackend +# ๐Ÿš€ Terraform S3 Static App Project ( Reusable and Self-bootstrapping) -# Just commenting random stuff for debugging +A **reusable, self-bootstrapping infrastructure template** for deploying modern **React (Vite) static applications** to AWS using: -- name: Debug GitHub Context - run: | - echo "Repository: ${{ github.repository }}" - echo "Ref: ${{ github.ref }}" - echo "Environment: prod" +- ๐Ÿชฃ 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. + +--- + +## ๐Ÿงฑ Architecture Overview + +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 + +--- + +## ๐Ÿ“ Project Structure + +``` +. +โ”œโ”€โ”€ 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 + +``` + +--- + +## โšก Features + +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 + +--- + +## ๐Ÿ“ฆ Prerequisites + +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+ +- GitHub repository +- AWS CLI configured (for local testing) + +--- + +## ๐Ÿš€ Getting Started + +1. Clone the repository + +``` +git clone https://github.com/ecoderP/s3-static-app-terraform.git +``` + +### Before you continue, Please note: + +- 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 + +cp terraform.tfvarsbackendexample terraform.tfvars +``` + +2. 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. + +4. Configure environment + +Each environment (dev/staging/prod) has its own configuration. Locate .tfbackend and .tfvars configuration files, personalise and rename for each environment. + +``` +cd terraform/environments/dev + +cp dev.tfbackendexample dev.tfbackend + +terraform init -backend-config=dev.tfbackend +``` + +5. Validate code and Deploy Infrastructure for each environment + +``` +terraform validate +terraform plan +terraform apply -auto-approve +``` + +--- + +## ๐Ÿ” GitHub OIDC Authentication + +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 + +### IAM Role Trust Relationship + +GitHub Actions assumes a role like: + +- Repository: Your-github-username/repo-name +- Branch-based conditions: + - dev โ†’ dev role + - staging โ†’ staging role + - main โ†’ production role + +--- + +## โš™๏ธ CI/CD Pipeline + +This project includes GitHub Actions workflows for: + +### ๐Ÿงช Dev Deployment + +- Trigger: push to dev +- Deploys to dev S3 bucket + CloudFront + +### ๐Ÿงฑ Staging Deployment + +- Trigger: push to staging +- Deploys to staging S3 bucket + CloudFront +- Used for pre-production validation + +### ๐Ÿš€ Production Deployment + +- Trigger: push to main +- Deploys stable build to production environment (S3 + CloudFront) + +### CI/CD Flow + +1. Checkout code +2. Install dependencies +3. Build Vite React app +4. Assume AWS role via OIDC +5. Sync build to S3 +6. Invalidate CloudFront cache + +### Important GitHub Actions secrets + +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 +``` + +--- + +## โ™ป๏ธ Re-using This Project (Some Viable Options) + +This repo is designed as a starter backend infrastructure for any React + Vite frontend project. + +### Option 1: Use as a Terraform Module + +``` +module "frontend_hosting" { + source = "github.com/ecoderP/s3-static-app-terraform//modules/s3-static-site" + + bucket_name = "my-new-app" + environment = "dev" +} +``` + +### Option 2: Multi-App Scaling + +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 + +--- + +## ๐Ÿ” Security Highlights + +- S3 bucket is private by default +- CloudFront serves as the only public entry point +- IAM follows least privilege principle + +--- + +## ๐Ÿงฑ Infrastructure Teardown + +1. From each environment directory (dev, staging and prod), run the command: + +``` +terraform destroy -auto-approve +``` + +Do this for all environments. + +2. From the terraform **_bootstrap_** directory, run the command: + +``` +terraform destroy -auto-approve + +``` + +3. Optional, but can do: List all AWS buckets in your account and confirmbuckets are not listed. Run the command: + +``` +aws s3 ls + +``` + +--- + +## ๐Ÿ“š Lessons Learned + +### 1. Infrastructure Modularity Matters Early + +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. + +### 2. OIDC Authentication Is More Secure Than Long-Lived AWS Keys + +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. + +### 3. Environment Isolation Prevents Deployment Drift + +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. + +### 4. Terraform State Management Requires Planning + +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 + +### 5. CI/CD Pipelines Are Infrastructure Too + +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. + +### 6. Small AWS Misconfigurations Can Cause Large Failures + +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. + +### 7. Reusability Requires Intentional Design + +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. + +### 8. Production Infrastructure Requires Both Security and Automation + +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. + +### 9. Documentation Is Part of Engineering + +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 + +--- + +## ๐Ÿ“ˆ Future Improvements + +- Add custom domain + Route53 automation +- ACM SSL certificate provisioning +- Automated performance testing in CI + +--- + +## ๐Ÿ‘จโ€๐Ÿ’ป Author + +Built and Maintained by [ecoderP](https://github.com/ecoderP) diff --git a/terraform/bootstrap/terraform.tfvarsexample b/terraform/bootstrap/terraform.tfvarsexample new file mode 100644 index 0000000..0bb43c2 --- /dev/null +++ b/terraform/bootstrap/terraform.tfvarsexample @@ -0,0 +1,4 @@ +aws_region = "Your AWS region" +project_name = "Your project name" +backend_bucket_name = "Your unique bucket name" +s3_encryption_algorithm = "AES256" # Default to SSE-S3 diff --git a/terraform/environments/dev/dev.tfbackendexample b/terraform/environments/dev/dev.tfbackendexample new file mode 100644 index 0000000..3d94a31 --- /dev/null +++ b/terraform/environments/dev/dev.tfbackendexample @@ -0,0 +1,5 @@ +bucket = "Your bootstrap backend state bucket name" +region = "Your AWS region" +key = "dev/terraform.tfstate" +encrypt = true +use_lockfile = true # important for S3 backend state lock \ No newline at end of file diff --git a/terraform/environments/dev/terraform.tfvarsexample b/terraform/environments/dev/terraform.tfvarsexample new file mode 100644 index 0000000..836681f --- /dev/null +++ b/terraform/environments/dev/terraform.tfvarsexample @@ -0,0 +1,6 @@ +aws_region = "Your AWS region" +project_name = "Project name" +website_bucket_name = "Your unique bucket name" # unique for each environment +github_repo = "Your-github-username/repo-name" # Not web link +github_branch = "dev" +environment = "dev" diff --git a/terraform/environments/prod/prod.tfbackendexample b/terraform/environments/prod/prod.tfbackendexample new file mode 100644 index 0000000..93dbd39 --- /dev/null +++ b/terraform/environments/prod/prod.tfbackendexample @@ -0,0 +1,5 @@ +bucket = "Your bootstrap backend state bucket name" +region = "Your AWS region" +key = "prod/terraform.tfstate" +encrypt = true +use_lockfile = true # important for S3 backend state lock \ No newline at end of file diff --git a/terraform/environments/prod/terraform.tfvarsexample b/terraform/environments/prod/terraform.tfvarsexample new file mode 100644 index 0000000..027ad41 --- /dev/null +++ b/terraform/environments/prod/terraform.tfvarsexample @@ -0,0 +1,6 @@ +aws_region = "Your AWS region" +project_name = "Project name" +website_bucket_name = "Your unique bucket name" +github_repo = "Your github username/repo name" # Not web link +github_branch = "prod" +environment = "prod" diff --git a/terraform/environments/staging/staging.tfbackendexample b/terraform/environments/staging/staging.tfbackendexample new file mode 100644 index 0000000..fb4c21e --- /dev/null +++ b/terraform/environments/staging/staging.tfbackendexample @@ -0,0 +1,5 @@ +bucket = "Your bootstrap backend state bucket name" +region = "Your AWS region" +key = "staging/terraform.tfstate" +encrypt = true +use_lockfile = true # important for S3 backend state lock \ No newline at end of file diff --git a/terraform/environments/staging/terraform.tfvarsexample b/terraform/environments/staging/terraform.tfvarsexample new file mode 100644 index 0000000..361647c --- /dev/null +++ b/terraform/environments/staging/terraform.tfvarsexample @@ -0,0 +1,6 @@ +aws_region = "Your AWS region" +project_name = "Project name" +website_bucket_name = "Your unique bucket name" # unique for each environment +github_repo = "Your github username/repo name" # Not web link +github_branch = "staging" +environment = "staging"