Migrate frontend from Amplify to CloudFront + S3
- Add CloudFront distribution with S3 origin - Configure S3 bucket with website hosting - Add Origin Access Control (OAC) for security - Configure SPA error handling (404/403 -> index.html) - Add Cloudflare DNS records (commented out due to invalid token) - Add deployment script for future updates - Update outputs to include CloudFront info
This commit is contained in:
69
scripts/deploy-frontend.sh
Executable file
69
scripts/deploy-frontend.sh
Executable file
@@ -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}"
|
||||||
161
terraform/cloudfront-frontend.tf
Normal file
161
terraform/cloudfront-frontend.tf
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
@@ -59,3 +59,24 @@ output "aws_region" {
|
|||||||
description = "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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user