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"
|
||||
}
|
||||
|
||||
# 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