personal-lifecycle-tracker / terraform / aws-serverless / deploy.sh
deploy.sh
Raw
#!/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=\"<your-key-id>\""
  echo "  export AWS_SECRET_ACCESS_KEY=\"<your-secret-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 "================================================================="