# 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)" } # Cloudflare Transform Rule to rewrite Host header for CloudFront resource "cloudflare_ruleset" "frontend_host_header_rewrite" { count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0 zone_id = data.cloudflare_zone.domain[0].id name = "Rewrite Host header for CloudFront" kind = "zone" phase = "http_request_late_transform" rules { action = "rewrite" expression = "(http.host eq \"${var.domain_name}\" or http.host eq \"www.${var.domain_name}\")" description = "Rewrite Host header to CloudFront domain" action_parameters { headers { name = "Host" operation = "set" value = aws_cloudfront_distribution.frontend.domain_name } } } }