#!/usr/bin/env bash set -euo pipefail # ============================================================================== # Zero-Cost Serverless Deployment Automation (AWS Lambda + S3 + CloudFront + Neon) # Enforces Universal Zero-Host-Runtime: 100% Containerized Tooling # ============================================================================== PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" TF_DIR="${PROJECT_ROOT}/terraform/aws-serverless" AWS_REGION="${AWS_DEFAULT_REGION:-ap-south-1}" AWS_CLI_IMAGE="amazon/aws-cli:latest" TF_IMAGE="hashicorp/terraform:1.7.5" NODE_IMAGE="node:20-alpine" echo "=================================================================" echo " Option C: Deploying Personal Lifecycle Tracker to AWS Serverless" echo "=================================================================" cd "${PROJECT_ROOT}" # 1. Pre-flight: Check AWS Credentials echo "==> [1/7] Verifying AWS Credentials in container..." if ! docker run --rm \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" sts get-caller-identity > /dev/null 2>&1; then echo "" echo "❌ Error: AWS Credentials not found or invalid!" echo "Please export your credentials before running deploy:" echo " export AWS_ACCESS_KEY_ID=\"\"" echo " export AWS_SECRET_ACCESS_KEY=\"\"" echo " export AWS_DEFAULT_REGION=\"us-east-1\"" echo "" echo "Or configure them via: aws configure" exit 1 fi echo " AWS credentials authenticated successfully." # 2. Pre-flight: Check terraform.tfvars echo "==> [2/7] Checking Terraform configuration variables..." if [ ! -f "${TF_DIR}/terraform.tfvars" ]; then echo " terraform.tfvars not found. Creating from template..." cp "${TF_DIR}/terraform.tfvars.example" "${TF_DIR}/terraform.tfvars" # Generate a cryptographically secure random secret key RANDOM_SECRET=$(head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n') sed -i "s/super-secret-jwt-key-replace-with-secure-random-32-byte-hex/${RANDOM_SECRET}/" "${TF_DIR}/terraform.tfvars" echo " Generated new secret_key in terraform.tfvars." fi # 3. Initialize & Validate Terraform echo "==> [3/7] Initializing Terraform..." docker run --rm \ -v "${TF_DIR}:/workspace" \ -w /workspace \ "${TF_IMAGE}" init # 4. Bootstrap ECR Repository & Push Backend Container Image echo "==> [4/7] Bootstrapping ECR Repository & Building Backend..." docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" apply -target=aws_ecr_repository.backend -target=aws_ecr_repository_policy.backend_lambda -auto-approve ECR_REPO_URL=$(docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" output -raw ecr_repository_url) echo " Authenticating Docker with Amazon ECR (${ECR_REPO_URL})..." docker run --rm \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" ecr get-login-password --region "${AWS_REGION}" | \ docker login --username AWS --password-stdin "${ECR_REPO_URL}" echo " Building Backend Container with AWS Lambda Web Adapter..." docker build -t "${ECR_REPO_URL}:latest" ./backend echo " Pushing image to Amazon ECR..." docker push "${ECR_REPO_URL}:latest" # 5. Full Terraform Provisioning (Lambda + S3 + CloudFront + EventBridge) echo "==> [5/7] Provisioning Serverless Infrastructure (Terraform Apply)..." docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" apply -auto-approve S3_BUCKET=$(docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" output -raw s3_bucket_name) CLOUDFRONT_DOMAIN=$(docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" output -raw cloudfront_domain_name) # 6. Build & Deploy Frontend SPA to S3 echo "==> [6/7] Building React 19 Frontend SPA in Container..." docker run --rm \ -v "${PROJECT_ROOT}/frontend:/app" \ -w /app \ "${NODE_IMAGE}" sh -c "npm install && npm run build" echo " Pre-compressing assets with Gzip for maximum performance..." docker run --rm \ -v "${PROJECT_ROOT}/frontend/dist:/dist" \ -w /dist \ alpine:latest sh -c "gzip -k -9 -f assets/*.js assets/*.css 2>/dev/null || true" echo " Syncing non-compressed files to S3 bucket (s3://${S3_BUCKET})..." docker run --rm \ -v "${PROJECT_ROOT}/frontend/dist:/dist" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" s3 sync /dist "s3://${S3_BUCKET}/" --exclude "*.gz" --delete echo " Uploading pre-compressed assets with Content-Encoding: gzip..." docker run --rm \ -v "${PROJECT_ROOT}/frontend/dist:/dist" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" s3 sync /dist/assets "s3://${S3_BUCKET}/assets/" --exclude "*" --include "*.js" --content-encoding gzip --content-type application/javascript docker run --rm \ -v "${PROJECT_ROOT}/frontend/dist:/dist" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" s3 sync /dist/assets "s3://${S3_BUCKET}/assets/" --exclude "*" --include "*.css" --content-encoding gzip --content-type text/css echo " Invalidating CloudFront distribution cache..." CLOUDFRONT_ID=$(docker run --rm \ -v "${TF_DIR}:/workspace" \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ -w /workspace \ "${TF_IMAGE}" output -raw cloudfront_distribution_id 2>/dev/null || echo "") if [ -n "${CLOUDFRONT_ID}" ]; then docker run --rm \ -v "${HOME}/.aws:/root/.aws:ro" \ -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ -e AWS_DEFAULT_REGION="${AWS_REGION}" \ "${AWS_CLI_IMAGE}" cloudfront create-invalidation --distribution-id "${CLOUDFRONT_ID}" --paths "/*" > /dev/null || true echo " CloudFront invalidation created for /*." fi # 7. Verification & Live Status echo "==> [7/7] Deployment complete! Verifying endpoints..." echo "" echo "=================================================================" echo " 🚀 DEPLOYMENT SUCCESSFUL" echo "=================================================================" echo " Web Application UI: https://${CLOUDFRONT_DOMAIN}" echo " Health Endpoint: https://${CLOUDFRONT_DOMAIN}/health" echo " API Documentation: https://${CLOUDFRONT_DOMAIN}/docs" echo " OpenAPI Schema: https://${CLOUDFRONT_DOMAIN}/openapi.json" echo " CloudWatch Warmer: Active (Every 4 mins - 0s cold starts)" echo " Daily Email Digest: Active (08:00 AM UTC via EventBridge)" echo " Cost: $0.00 / month forever" echo "================================================================="