diff --git a/scripts/deploy-frontend.sh b/scripts/deploy-frontend.sh new file mode 100755 index 0000000..055b66b --- /dev/null +++ b/scripts/deploy-frontend.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Frontend deployment script for CloudFront + S3 +# This script builds the Next.js app and deploys it to S3 + CloudFront + +set -e + +echo "🚀 Starting frontend deployment..." + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Configuration +S3_BUCKET="filamenteka-frontend" +REGION="eu-central-1" + +# Get CloudFront distribution ID from terraform output +echo -e "${BLUE}📋 Getting CloudFront distribution ID...${NC}" +DISTRIBUTION_ID=$(terraform -chdir=terraform output -raw cloudfront_distribution_id 2>/dev/null || terraform output -raw cloudfront_distribution_id) + +if [ -z "$DISTRIBUTION_ID" ]; then + echo -e "${RED}❌ Could not get CloudFront distribution ID${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Distribution ID: $DISTRIBUTION_ID${NC}" + +# Build the Next.js app +echo -e "${BLUE}🔨 Building Next.js app...${NC}" +npm run build + +if [ ! -d "out" ]; then + echo -e "${RED}❌ Build failed - 'out' directory not found${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Build completed${NC}" + +# Upload to S3 +echo -e "${BLUE}📤 Uploading to S3...${NC}" +aws s3 sync out/ s3://$S3_BUCKET/ \ + --region $REGION \ + --delete \ + --cache-control "public, max-age=3600" + +echo -e "${GREEN}✓ Files uploaded to S3${NC}" + +# Invalidate CloudFront cache +echo -e "${BLUE}🔄 Invalidating CloudFront cache...${NC}" +INVALIDATION_ID=$(aws cloudfront create-invalidation \ + --distribution-id $DISTRIBUTION_ID \ + --paths "/*" \ + --query 'Invalidation.Id' \ + --output text) + +echo -e "${GREEN}✓ CloudFront invalidation created: $INVALIDATION_ID${NC}" + +# Get CloudFront URL +CF_URL=$(terraform -chdir=terraform output -raw cloudfront_domain_name 2>/dev/null || terraform output -raw cloudfront_domain_name) + +echo "" +echo -e "${GREEN}✅ Deployment complete!${NC}" +echo -e "${BLUE}🌐 CloudFront URL: https://$CF_URL${NC}" +echo -e "${BLUE}🌐 Custom Domain: https://filamenteka.rs (after DNS update)${NC}" +echo "" +echo -e "${BLUE}â„šī¸ Note: CloudFront cache invalidation may take 5-10 minutes to propagate globally.${NC}" diff --git a/terraform/cloudfront-frontend.tf b/terraform/cloudfront-frontend.tf new file mode 100644 index 0000000..ade4d29 --- /dev/null +++ b/terraform/cloudfront-frontend.tf @@ -0,0 +1,161 @@ +# S3 bucket for static website hosting +resource "aws_s3_bucket" "frontend" { + bucket = "${var.app_name}-frontend" + + tags = { + Name = "${var.app_name}-frontend" + Environment = var.environment + } +} + +# S3 bucket website configuration +resource "aws_s3_bucket_website_configuration" "frontend" { + bucket = aws_s3_bucket.frontend.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "404.html" + } +} + +# S3 bucket public access block (we'll use CloudFront OAC instead) +resource "aws_s3_bucket_public_access_block" "frontend" { + bucket = aws_s3_bucket.frontend.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# CloudFront Origin Access Control +resource "aws_cloudfront_origin_access_control" "frontend" { + name = "${var.app_name}-frontend-oac" + description = "OAC for ${var.app_name} frontend" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# CloudFront distribution +resource "aws_cloudfront_distribution" "frontend" { + enabled = true + is_ipv6_enabled = true + comment = "${var.app_name} frontend" + default_root_object = "index.html" + price_class = "PriceClass_100" # US, Canada, Europe only (cheapest) + + # No aliases - Cloudflare will proxy to CloudFront's default domain + # aliases = var.domain_name != "" ? [var.domain_name, "www.${var.domain_name}"] : [] + + origin { + domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name + origin_id = "S3-${aws_s3_bucket.frontend.id}" + origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-${aws_s3_bucket.frontend.id}" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 3600 # 1 hour + max_ttl = 86400 # 24 hours + compress = true + } + + # Custom error responses for SPA routing + custom_error_response { + error_code = 404 + response_code = 200 + response_page_path = "/index.html" + error_caching_min_ttl = 300 + } + + custom_error_response { + error_code = 403 + response_code = 200 + response_page_path = "/index.html" + error_caching_min_ttl = 300 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + # Use default CloudFront certificate (we'll handle SSL via Cloudflare) + viewer_certificate { + cloudfront_default_certificate = true + # If you want CloudFront SSL, add ACM certificate here + # acm_certificate_arn = aws_acm_certificate.cert.arn + # ssl_support_method = "sni-only" + # minimum_protocol_version = "TLSv1.2_2021" + } + + tags = { + Name = "${var.app_name}-frontend" + Environment = var.environment + } +} + +# S3 bucket policy to allow CloudFront OAC access +resource "aws_s3_bucket_policy" "frontend" { + bucket = aws_s3_bucket.frontend.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontServicePrincipal" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.frontend.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn + } + } + } + ] + }) +} + +# Cloudflare DNS records for frontend +resource "cloudflare_record" "frontend_root" { + count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0 + zone_id = data.cloudflare_zone.domain[0].id + name = "@" + type = "CNAME" + value = aws_cloudfront_distribution.frontend.domain_name + ttl = 1 + proxied = true # Enable Cloudflare proxy for SSL and caching + comment = "CloudFront distribution for frontend" +} + +resource "cloudflare_record" "frontend_www" { + count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0 + zone_id = data.cloudflare_zone.domain[0].id + name = "www" + type = "CNAME" + value = aws_cloudfront_distribution.frontend.domain_name + ttl = 1 + proxied = true # Enable Cloudflare proxy for SSL and caching + comment = "CloudFront distribution for frontend (www)" +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index cb9ca33..a333c92 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -59,3 +59,24 @@ output "aws_region" { description = "AWS Region" } +# CloudFront + S3 Frontend Outputs +output "s3_bucket_name" { + value = aws_s3_bucket.frontend.id + description = "S3 bucket name for frontend" +} + +output "cloudfront_distribution_id" { + value = aws_cloudfront_distribution.frontend.id + description = "CloudFront distribution ID" +} + +output "cloudfront_domain_name" { + value = aws_cloudfront_distribution.frontend.domain_name + description = "CloudFront distribution domain name" +} + +output "frontend_url" { + value = var.domain_name != "" ? "https://${var.domain_name}" : "https://${aws_cloudfront_distribution.frontend.domain_name}" + description = "Frontend URL" +} +