From 2fc93dda7372b5c740b3fc158b62c73ff8bae53a Mon Sep 17 00:00:00 2001 From: Script47 Date: Sat, 4 Apr 2026 22:35:59 +0100 Subject: [PATCH 1/5] fix(static-site): fix bugs and refactor - Fix instance key issues - Ensure the certificate is created before being destroyed - Perform certificate validation only if the domains match those in Route 53 - Standardise and clean up naming conventions for consistency --- static-site/acm.tf | 15 +++++++++++---- static-site/cloudfront.tf | 16 ++++++++++------ static-site/data.tf | 13 +++++++++++-- static-site/locals.tf | 12 ++++++++---- static-site/outputs.tf | 16 ++++++++++++++-- static-site/route53.tf | 21 +++++++++++++-------- 6 files changed, 67 insertions(+), 26 deletions(-) diff --git a/static-site/acm.tf b/static-site/acm.tf index f542091..080473e 100644 --- a/static-site/acm.tf +++ b/static-site/acm.tf @@ -4,10 +4,17 @@ resource "aws_acm_certificate" "cloudfront_cert" { validation_method = "DNS" tags = var.tags provider = aws.acm + + lifecycle { + create_before_destroy = true + } } resource "aws_acm_certificate_validation" "cloudfront_cert_validation" { - certificate_arn = aws_acm_certificate.cloudfront_cert.arn - validation_record_fqdns = [for record in aws_route53_record.acm_records : record.fqdn] - provider = aws.acm -} \ No newline at end of file + certificate_arn = aws_acm_certificate.cloudfront_cert.arn + validation_record_fqdns = [ + for record in aws_route53_record.acm_records : record.fqdn + if contains(local.internal_domains, record.fqdn) + ] + provider = aws.acm +} diff --git a/static-site/cloudfront.tf b/static-site/cloudfront.tf index c501165..dcff185 100644 --- a/static-site/cloudfront.tf +++ b/static-site/cloudfront.tf @@ -1,5 +1,9 @@ locals { - origin_id = "S3-${aws_s3_bucket.static_site.bucket}" + origin_id = "S3-${local.bucket_name}" + origin_path = var.origin_path == "" ? "" : var.origin_path + bucket_regional_domain_name = var.create_bucket ? aws_s3_bucket.static_site[0].bucket_regional_domain_name : data.aws_s3_bucket.user_created[0].bucket_regional_domain_name + oac_name = replace("${local.bucket_name}-${replace(local.origin_path, "/", "-")}", "/[^a-zA-Z0-9-]/", "") + rhp_name = local.primary_domain_normalised } resource "aws_cloudfront_distribution" "static_site" { @@ -26,8 +30,8 @@ resource "aws_cloudfront_distribution" "static_site" { origin { origin_id = local.origin_id origin_access_control_id = aws_cloudfront_origin_access_control.oac.id - domain_name = aws_s3_bucket.static_site.bucket_regional_domain_name - origin_path = var.origin_path != "" ? "/${var.origin_path}" : "" + domain_name = local.bucket_regional_domain_name + origin_path = local.origin_path } default_cache_behavior { @@ -68,8 +72,8 @@ resource "aws_cloudfront_distribution" "static_site" { } resource "aws_cloudfront_origin_access_control" "oac" { - name = "oac-for-${aws_s3_bucket.static_site.bucket}" - description = "OAC for ${aws_s3_bucket.static_site.bucket}" + name = local.oac_name + description = "OAC for ${local.bucket_name}" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" @@ -77,7 +81,7 @@ resource "aws_cloudfront_origin_access_control" "oac" { } resource "aws_cloudfront_response_headers_policy" "cloudfront" { - name = "cf-resp-hdrs-${local.primary_domain_normalised}" + name = local.rhp_name comment = "Response headers policy for ${local.primary_domain}" cors_config { diff --git a/static-site/data.tf b/static-site/data.tf index c0a8c67..0b57f1c 100644 --- a/static-site/data.tf +++ b/static-site/data.tf @@ -1,3 +1,7 @@ +locals { + custom_prefix = startswith(local.origin_path, "/") ? substr(local.origin_path, 1, length(local.origin_path)) : local.origin_path + bucket_prefix = local.origin_path == "" ? "*" : "${local.custom_prefix}/*" +} data "aws_caller_identity" "current" {} data "aws_route53_zone" "hosted_zone" { @@ -6,11 +10,16 @@ data "aws_route53_zone" "hosted_zone" { private_zone = false } +data "aws_s3_bucket" "user_created" { + count = !var.create_bucket ? 1 : 0 + bucket = var.bucket_name +} + data "aws_iam_policy_document" "cloudfront_to_s3" { statement { sid = "AllowCloudFrontToAccessBucket" actions = ["s3:GetObject"] - resources = ["${aws_s3_bucket.static_site.arn}/*"] + resources = ["${local.bucket_arn}/${local.bucket_prefix}"] principals { type = "Service" @@ -29,4 +38,4 @@ data "aws_iam_policy_document" "cloudfront_to_s3" { values = [data.aws_caller_identity.current.account_id] } } -} \ No newline at end of file +} diff --git a/static-site/locals.tf b/static-site/locals.tf index b661838..d797a8d 100644 --- a/static-site/locals.tf +++ b/static-site/locals.tf @@ -1,10 +1,14 @@ locals { + create_hosted_zone = var.hosted_zone == "" + hosted_zone_domain = local.create_hosted_zone ? local.primary_domain : data.aws_route53_zone.hosted_zone[0].name domains = distinct(var.domains) primary_domain = local.domains[0] - - primary_domain_normalised = replace(local.primary_domain, ".", "-") - - create_hosted_zone = var.hosted_zone == "" + primary_domain_normalised = replace(replace(local.primary_domain, ".", "-"), "/[^a-zA-Z0-9-]/", "") + internal_domains = [ + for d in local.domains : d + if endswith(d, local.hosted_zone_domain) + ] bucket_name = var.bucket_name == "" ? local.primary_domain : var.bucket_name + bucket_arn = var.create_bucket ? aws_s3_bucket.static_site[0].arn : data.aws_s3_bucket.user_created[0].arn } diff --git a/static-site/outputs.tf b/static-site/outputs.tf index 763f348..c64e769 100644 --- a/static-site/outputs.tf +++ b/static-site/outputs.tf @@ -1,7 +1,7 @@ output "bucket" { value = { - arn = aws_s3_bucket.static_site.arn - id = aws_s3_bucket.static_site.id + arn = var.create_bucket ? aws_s3_bucket.static_site[0].arn : data.aws_s3_bucket.user_created[0].arn + id = local.bucket_name } } @@ -13,3 +13,15 @@ output "cloudfront" { aliases = aws_cloudfront_distribution.static_site.aliases } } + +output "external_validation_records" { + value = { + for dvo in aws_acm_certificate.cloudfront_cert.domain_validation_options : + dvo.domain_name => { + name = dvo.resource_record_name + value = dvo.resource_record_value + type = dvo.resource_record_type + } + if !contains(local.internal_domains, dvo.domain_name) + } +} \ No newline at end of file diff --git a/static-site/route53.tf b/static-site/route53.tf index 29e8585..6dce461 100644 --- a/static-site/route53.tf +++ b/static-site/route53.tf @@ -9,20 +9,23 @@ resource "aws_route53_zone" "hosted_zone" { ############################################# resource "aws_route53_record" "acm_records" { for_each = { - for dvo in aws_acm_certificate.cloudfront_cert.domain_validation_options : dvo.domain_name => { + for dvo in aws_acm_certificate.cloudfront_cert.domain_validation_options : + dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } - zone_id = local.create_hosted_zone ? aws_route53_zone.hosted_zone[0].zone_id : data.aws_route53_zone.hosted_zone[0].zone_id - type = each.value.type - name = each.value.name - records = [each.value.record] - ttl = 60 + zone_id = try(aws_route53_zone.hosted_zone[0].zone_id, data.aws_route53_zone.hosted_zone[0].zone_id) + type = each.value.type + name = each.value.name + records = [each.value.record] + ttl = 60 + allow_overwrite = true - provider = aws.default + + provider = aws.default } ############################################# @@ -31,7 +34,7 @@ resource "aws_route53_record" "acm_records" { resource "aws_route53_record" "static_site_a_record" { count = length(local.domains) - zone_id = local.create_hosted_zone ? aws_route53_zone.hosted_zone[0].zone_id : data.aws_route53_zone.hosted_zone[0].zone_id + zone_id = try(aws_route53_zone.hosted_zone[0].zone_id, data.aws_route53_zone.hosted_zone[0].zone_id) type = "A" name = local.domains[count.index] @@ -41,5 +44,7 @@ resource "aws_route53_record" "static_site_a_record" { evaluate_target_health = false } + allow_overwrite = true + provider = aws.default } From 9318a2f0c364c19f49771a9a28f7e5cb5630f677 Mon Sep 17 00:00:00 2001 From: Script47 Date: Wed, 8 Apr 2026 18:03:09 +0100 Subject: [PATCH 2/5] add output for debugging --- static-site/locals.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static-site/locals.tf b/static-site/locals.tf index d797a8d..aa694de 100644 --- a/static-site/locals.tf +++ b/static-site/locals.tf @@ -12,3 +12,7 @@ locals { bucket_name = var.bucket_name == "" ? local.primary_domain : var.bucket_name bucket_arn = var.create_bucket ? aws_s3_bucket.static_site[0].arn : data.aws_s3_bucket.user_created[0].arn } + +output "internal_domains" { + value = local.internal_domains +} From 2e98c98084d837c3365b34be367ec53b3092a724 Mon Sep 17 00:00:00 2001 From: Script47 Date: Wed, 8 Apr 2026 22:21:58 +0100 Subject: [PATCH 3/5] origin id rename --- static-site/cloudfront.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static-site/cloudfront.tf b/static-site/cloudfront.tf index dcff185..5575144 100644 --- a/static-site/cloudfront.tf +++ b/static-site/cloudfront.tf @@ -1,5 +1,5 @@ locals { - origin_id = "S3-${local.bucket_name}" + origin_id = "s3-static-site" origin_path = var.origin_path == "" ? "" : var.origin_path bucket_regional_domain_name = var.create_bucket ? aws_s3_bucket.static_site[0].bucket_regional_domain_name : data.aws_s3_bucket.user_created[0].bucket_regional_domain_name oac_name = replace("${local.bucket_name}-${replace(local.origin_path, "/", "-")}", "/[^a-zA-Z0-9-]/", "") From 6cedf65d905a9612cf8d0d585538085e07d3bf4e Mon Sep 17 00:00:00 2001 From: Script47 Date: Wed, 8 Apr 2026 22:35:08 +0100 Subject: [PATCH 4/5] rename + formatting --- static-site/acm.tf | 8 ++++---- static-site/cloudfront.tf | 26 +++++++++++++++----------- static-site/data.tf | 4 ++-- static-site/locals.tf | 14 ++++++++------ static-site/outputs.tf | 12 ++++++------ static-site/route53.tf | 16 ++++++++-------- static-site/s3.tf | 12 ++++++------ 7 files changed, 49 insertions(+), 43 deletions(-) diff --git a/static-site/acm.tf b/static-site/acm.tf index 080473e..fbfc968 100644 --- a/static-site/acm.tf +++ b/static-site/acm.tf @@ -1,6 +1,6 @@ -resource "aws_acm_certificate" "cloudfront_cert" { +resource "aws_acm_certificate" "this" { domain_name = local.primary_domain - subject_alternative_names = local.domains + subject_alternative_names = var.domains validation_method = "DNS" tags = var.tags provider = aws.acm @@ -10,8 +10,8 @@ resource "aws_acm_certificate" "cloudfront_cert" { } } -resource "aws_acm_certificate_validation" "cloudfront_cert_validation" { - certificate_arn = aws_acm_certificate.cloudfront_cert.arn +resource "aws_acm_certificate_validation" "this" { + certificate_arn = aws_acm_certificate.this.arn validation_record_fqdns = [ for record in aws_route53_record.acm_records : record.fqdn if contains(local.internal_domains, record.fqdn) diff --git a/static-site/cloudfront.tf b/static-site/cloudfront.tf index 5575144..5c66ad6 100644 --- a/static-site/cloudfront.tf +++ b/static-site/cloudfront.tf @@ -1,20 +1,24 @@ locals { origin_id = "s3-static-site" origin_path = var.origin_path == "" ? "" : var.origin_path - bucket_regional_domain_name = var.create_bucket ? aws_s3_bucket.static_site[0].bucket_regional_domain_name : data.aws_s3_bucket.user_created[0].bucket_regional_domain_name - oac_name = replace("${local.bucket_name}-${replace(local.origin_path, "/", "-")}", "/[^a-zA-Z0-9-]/", "") - rhp_name = local.primary_domain_normalised + bucket_regional_domain_name = var.create_bucket ? aws_s3_bucket.this[0].bucket_regional_domain_name : data.aws_s3_bucket.user_created[0].bucket_regional_domain_name + oac_name = "${replace( + replace("${local.bucket_name}-${replace(local.origin_path, "/", "-")}-oac", "--", "-"), + "/[^a-zA-Z0-9.-]/", + "" + )}-oac" + rhp_name = "${local.primary_domain_normalised}-cf-rhp" } -resource "aws_cloudfront_distribution" "static_site" { +resource "aws_cloudfront_distribution" "this" { comment = "Distribution for ${local.primary_domain}" - aliases = local.domains + aliases = var.domains enabled = true is_ipv6_enabled = true default_root_object = "index.html" viewer_certificate { - acm_certificate_arn = aws_acm_certificate.cloudfront_cert.arn + acm_certificate_arn = aws_acm_certificate.this.arn ssl_support_method = "sni-only" minimum_protocol_version = var.viewer_certificate.minimum_protocol_version cloudfront_default_certificate = false @@ -29,14 +33,14 @@ resource "aws_cloudfront_distribution" "static_site" { origin { origin_id = local.origin_id - origin_access_control_id = aws_cloudfront_origin_access_control.oac.id + origin_access_control_id = aws_cloudfront_origin_access_control.this.id domain_name = local.bucket_regional_domain_name origin_path = local.origin_path } default_cache_behavior { target_origin_id = local.origin_id - response_headers_policy_id = aws_cloudfront_response_headers_policy.cloudfront.id + response_headers_policy_id = aws_cloudfront_response_headers_policy.this.id allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] @@ -67,11 +71,11 @@ resource "aws_cloudfront_distribution" "static_site" { } tags = var.tags - depends_on = [aws_acm_certificate_validation.cloudfront_cert_validation] + depends_on = [aws_acm_certificate_validation.this] provider = aws.default } -resource "aws_cloudfront_origin_access_control" "oac" { +resource "aws_cloudfront_origin_access_control" "this" { name = local.oac_name description = "OAC for ${local.bucket_name}" origin_access_control_origin_type = "s3" @@ -80,7 +84,7 @@ resource "aws_cloudfront_origin_access_control" "oac" { provider = aws.default } -resource "aws_cloudfront_response_headers_policy" "cloudfront" { +resource "aws_cloudfront_response_headers_policy" "this" { name = local.rhp_name comment = "Response headers policy for ${local.primary_domain}" diff --git a/static-site/data.tf b/static-site/data.tf index 0b57f1c..36512fb 100644 --- a/static-site/data.tf +++ b/static-site/data.tf @@ -4,7 +4,7 @@ locals { } data "aws_caller_identity" "current" {} -data "aws_route53_zone" "hosted_zone" { +data "aws_route53_zone" "this" { count = local.create_hosted_zone ? 0 : 1 name = var.hosted_zone private_zone = false @@ -29,7 +29,7 @@ data "aws_iam_policy_document" "cloudfront_to_s3" { condition { test = "StringEquals" variable = "AWS:SourceArn" - values = [aws_cloudfront_distribution.static_site.arn] + values = [aws_cloudfront_distribution.this.arn] } condition { diff --git a/static-site/locals.tf b/static-site/locals.tf index aa694de..2bf73fa 100644 --- a/static-site/locals.tf +++ b/static-site/locals.tf @@ -1,16 +1,18 @@ locals { create_hosted_zone = var.hosted_zone == "" - hosted_zone_domain = local.create_hosted_zone ? local.primary_domain : data.aws_route53_zone.hosted_zone[0].name - domains = distinct(var.domains) - primary_domain = local.domains[0] - primary_domain_normalised = replace(replace(local.primary_domain, ".", "-"), "/[^a-zA-Z0-9-]/", "") + hosted_zone_domain = local.create_hosted_zone ? local.primary_domain : data.aws_route53_zone.this[0].name + primary_domain = var.domains[0] + primary_domain_normalised = replace( + replace(local.primary_domain, "/\\*\\./", ""), # remove wildcard prefix + "/[^a-zA-Z0-9-]/", "-" # replace anything else with - + ) internal_domains = [ - for d in local.domains : d + for d in var.domains : d if endswith(d, local.hosted_zone_domain) ] bucket_name = var.bucket_name == "" ? local.primary_domain : var.bucket_name - bucket_arn = var.create_bucket ? aws_s3_bucket.static_site[0].arn : data.aws_s3_bucket.user_created[0].arn + bucket_arn = var.create_bucket ? aws_s3_bucket.this[0].arn : data.aws_s3_bucket.user_created[0].arn } output "internal_domains" { diff --git a/static-site/outputs.tf b/static-site/outputs.tf index c64e769..27464c3 100644 --- a/static-site/outputs.tf +++ b/static-site/outputs.tf @@ -1,22 +1,22 @@ output "bucket" { value = { - arn = var.create_bucket ? aws_s3_bucket.static_site[0].arn : data.aws_s3_bucket.user_created[0].arn + arn = var.create_bucket ? aws_s3_bucket.this[0].arn : data.aws_s3_bucket.user_created[0].arn id = local.bucket_name } } output "cloudfront" { value = { - arn = aws_cloudfront_distribution.static_site.arn - id = aws_cloudfront_distribution.static_site.id - domain_name = aws_cloudfront_distribution.static_site.domain_name - aliases = aws_cloudfront_distribution.static_site.aliases + arn = aws_cloudfront_distribution.this.arn + id = aws_cloudfront_distribution.this.id + domain_name = aws_cloudfront_distribution.this.domain_name + aliases = aws_cloudfront_distribution.this.aliases } } output "external_validation_records" { value = { - for dvo in aws_acm_certificate.cloudfront_cert.domain_validation_options : + for dvo in aws_acm_certificate.this.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name value = dvo.resource_record_value diff --git a/static-site/route53.tf b/static-site/route53.tf index 6dce461..1fcdcec 100644 --- a/static-site/route53.tf +++ b/static-site/route53.tf @@ -1,4 +1,4 @@ -resource "aws_route53_zone" "hosted_zone" { +resource "aws_route53_zone" "this" { count = local.create_hosted_zone ? 1 : 0 name = local.primary_domain tags = var.tags @@ -9,7 +9,7 @@ resource "aws_route53_zone" "hosted_zone" { ############################################# resource "aws_route53_record" "acm_records" { for_each = { - for dvo in aws_acm_certificate.cloudfront_cert.domain_validation_options : + for dvo in aws_acm_certificate.this.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value @@ -17,7 +17,7 @@ resource "aws_route53_record" "acm_records" { } } - zone_id = try(aws_route53_zone.hosted_zone[0].zone_id, data.aws_route53_zone.hosted_zone[0].zone_id) + zone_id = try(aws_route53_zone.this[0].zone_id, data.aws_route53_zone.this[0].zone_id) type = each.value.type name = each.value.name records = [each.value.record] @@ -32,15 +32,15 @@ resource "aws_route53_record" "acm_records" { # Setup the A record for your custom domain ############################################# resource "aws_route53_record" "static_site_a_record" { - count = length(local.domains) + count = length(var.domains) - zone_id = try(aws_route53_zone.hosted_zone[0].zone_id, data.aws_route53_zone.hosted_zone[0].zone_id) + zone_id = try(aws_route53_zone.this[0].zone_id, data.aws_route53_zone.this[0].zone_id) type = "A" - name = local.domains[count.index] + name = var.domains[count.index] alias { - name = aws_cloudfront_distribution.static_site.domain_name - zone_id = aws_cloudfront_distribution.static_site.hosted_zone_id + name = aws_cloudfront_distribution.this.domain_name + zone_id = aws_cloudfront_distribution.this.hosted_zone_id evaluate_target_health = false } diff --git a/static-site/s3.tf b/static-site/s3.tf index 3a3d346..3b70367 100644 --- a/static-site/s3.tf +++ b/static-site/s3.tf @@ -1,4 +1,4 @@ -resource "aws_s3_bucket" "static_site" { +resource "aws_s3_bucket" "this" { count = var.create_bucket ? 1 : 0 bucket = local.bucket_name @@ -7,8 +7,8 @@ resource "aws_s3_bucket" "static_site" { provider = aws.default } -resource "aws_s3_bucket_public_access_block" "static_site" { - bucket = var.bucket_name != "" ? var.bucket_name : aws_s3_bucket.static_site[0].id +resource "aws_s3_bucket_public_access_block" "this" { + bucket = var.bucket_name != "" ? var.bucket_name : aws_s3_bucket.this[0].id block_public_acls = true block_public_policy = true ignore_public_acls = true @@ -16,8 +16,8 @@ resource "aws_s3_bucket_public_access_block" "static_site" { provider = aws.default } -resource "aws_s3_bucket_policy" "static_site" { - bucket = var.bucket_name != "" ? var.bucket_name : aws_s3_bucket.static_site[0].bucket +resource "aws_s3_bucket_policy" "this" { + bucket = var.bucket_name != "" ? var.bucket_name : aws_s3_bucket.this[0].bucket policy = data.aws_iam_policy_document.cloudfront_to_s3.json provider = aws.default -} \ No newline at end of file +} From 93c97639e83e1599e62ce43e43b16217f57854d8 Mon Sep 17 00:00:00 2001 From: Script47 Date: Wed, 8 Apr 2026 22:42:53 +0100 Subject: [PATCH 5/5] fix oac name --- static-site/cloudfront.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static-site/cloudfront.tf b/static-site/cloudfront.tf index 5c66ad6..8030fde 100644 --- a/static-site/cloudfront.tf +++ b/static-site/cloudfront.tf @@ -2,11 +2,11 @@ locals { origin_id = "s3-static-site" origin_path = var.origin_path == "" ? "" : var.origin_path bucket_regional_domain_name = var.create_bucket ? aws_s3_bucket.this[0].bucket_regional_domain_name : data.aws_s3_bucket.user_created[0].bucket_regional_domain_name - oac_name = "${replace( + oac_name = replace( replace("${local.bucket_name}-${replace(local.origin_path, "/", "-")}-oac", "--", "-"), "/[^a-zA-Z0-9.-]/", "" - )}-oac" + ) rhp_name = "${local.primary_domain_normalised}-cf-rhp" }