Skip to content

Commit 53879dd

Browse files
authored
feat(static-site): allow specifying multiple domains (#4)
- add support for multiple domains - add support for 404 error fallbacks - enforce validation rules on variables - update outputs format - update readme
1 parent f275570 commit 53879dd

File tree

7 files changed

+59
-37
lines changed

7 files changed

+59
-37
lines changed

static-site/README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
## About
44

5-
This module enables you to set up a static site/SPA on AWS with the following features:
6-
7-
- Custom domain with corresponding A record in the hosted zone and TLS certificate (DNS validated)
8-
- S3 bucket for hosting the static site
9-
- CloudFront distribution for site delivery
10-
- OAC for secure access between CloudFront and S3
11-
- Ensures only signed requests (SigV4) are allowed to S3
12-
- IAM role for CD (e.g., GitHub Actions)
5+
This module allows you to setup a static site with the following features:
6+
7+
- S3 bucket for static content (secure, private access only via CloudFront OAC)
8+
- CloudFront distribution with TLS certificate (ACM) and full domain aliasing
9+
- Automatic DNS validation for ACM via Route 53
10+
- Optional creation of Route 53 Hosted Zone (or reuse an existing one)
11+
- Multi-domain support
12+
- CI/CD deploy IAM role via OIDC (optional)
13+
- Fallback support for SPA (`403 → 200 /index.html` and `404 → 200 /index.html`)
1314
- Resource tagging for manageable resources
1415

1516
## Assumptions
@@ -19,19 +20,21 @@ toggle to disable this.
1920

2021
## Usage
2122

23+
See `variables.tf` for the full argument reference.
24+
2225
```hcl
2326
module "static_site" {
24-
source = "github.com/Script47/aws-tf-modules/static-site"
27+
source = "github.com/Script47/aws-tf-modules/static-site"
2528
29+
domains = ["example.org"]
2630
bucket_name = "example.org"
2731
hosted_zone = "my-hosted_zone"
28-
domain_name = "example.org"
2932
role_name = "deploy-example-org"
3033
repo = "example-org/repo:ref:refs/heads/master"
3134
setup_cd = false
3235
3336
restriction = {
34-
type = "none"
37+
type = "none"
3538
locations = []
3639
}
3740
@@ -42,7 +45,7 @@ module "static_site" {
4245
tags = {
4346
Project = "my-project"
4447
Service = "my-service"
45-
Environment = "prod"
48+
Environment = "produdction"
4649
}
4750
4851
providers = {

static-site/acm.tf

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
resource "aws_acm_certificate" "cloudfront_cert" {
2-
domain_name = var.domain_name
3-
validation_method = "DNS"
4-
tags = var.tags
5-
provider = aws.acm
2+
domain_name = local.primary_domain
3+
subject_alternative_names = local.domains
4+
validation_method = "DNS"
5+
tags = var.tags
6+
provider = aws.acm
67
}
78

89
resource "aws_acm_certificate_validation" "cloudfront_cert_validation" {

static-site/cloudfront.tf

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
resource "aws_cloudfront_distribution" "static_site" {
2-
comment = "Distribution for ${var.domain_name}"
3-
aliases = local.aliases
2+
comment = "Distribution for ${local.primary_domain}"
3+
aliases = local.domains
44
enabled = true
55
is_ipv6_enabled = true
66
default_root_object = "index.html"
@@ -43,11 +43,17 @@ resource "aws_cloudfront_distribution" "static_site" {
4343
cloudfront_default_certificate = false
4444
}
4545

46-
# For SPAs this is needed for hard refresh on dynamic routes
4746
custom_error_response {
48-
response_page_path = "/index.html"
49-
response_code = 404
5047
error_code = 403
48+
response_code = 200
49+
response_page_path = "/index.html"
50+
error_caching_min_ttl = 0
51+
}
52+
53+
custom_error_response {
54+
error_code = 404
55+
response_code = 200
56+
response_page_path = "/index.html"
5157
error_caching_min_ttl = 0
5258
}
5359

@@ -67,7 +73,7 @@ resource "aws_cloudfront_origin_access_control" "oac" {
6773

6874
resource "aws_cloudfront_response_headers_policy" "cloudfront" {
6975
name = "cloudfront-response-headers-policy"
70-
comment = "Response headers policy for ${var.domain_name}"
76+
comment = "Response headers policy for ${local.primary_domain}"
7177

7278
security_headers_config {
7379
content_type_options {

static-site/locals.tf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
locals {
2-
aliases = [var.domain_name]
2+
domains = distinct(var.domains)
3+
primary_domain = local.domains[0]
34
create_hosted_zone = var.hosted_zone == ""
4-
bucket_name = var.bucket_name == "" ? var.domain_name : var.bucket_name
5+
bucket_name = var.bucket_name == "" ? local.primary_domain : var.bucket_name
56
}

static-site/outputs.tf

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
output "bucket_name" {
2-
value = aws_s3_bucket.static_site.bucket
3-
}
4-
5-
output "deploy_role_arn" {
6-
value = var.setup_cd ? aws_iam_role.deploy_static_site[0].arn : null
1+
output "bucket" {
2+
value = {
3+
id = aws_s3_bucket.static_site.id
4+
arn = aws_s3_bucket.static_site.arn
5+
}
76
}
87

98
output "cloudfront" {
@@ -12,4 +11,10 @@ output "cloudfront" {
1211
domain_name = aws_cloudfront_distribution.static_site.domain_name
1312
aliases = aws_cloudfront_distribution.static_site.aliases
1413
}
15-
}
14+
}
15+
16+
output "role" {
17+
value = var.setup_cd ? {
18+
arn = aws_iam_role.deploy_static_site[0].arn
19+
} : null
20+
}

static-site/route53.tf

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
resource "aws_route53_zone" "hosted_zone" {
22
count = local.create_hosted_zone ? 1 : 0
3-
name = var.domain_name
3+
name = local.primary_domain
44
tags = var.tags
55
}
66

@@ -16,8 +16,8 @@ resource "aws_route53_record" "acm_records" {
1616
}
1717
}
1818

19-
type = each.value.type
2019
zone_id = local.create_hosted_zone ? aws_route53_zone.hosted_zone[0].zone_id : data.aws_route53_zone.hosted_zone[0].zone_id
20+
type = each.value.type
2121
name = each.value.name
2222
records = [each.value.record]
2323
ttl = 60
@@ -29,9 +29,11 @@ resource "aws_route53_record" "acm_records" {
2929
# Setup the A record for your custom domain
3030
#############################################
3131
resource "aws_route53_record" "static_site_a_record" {
32+
count = length(local.domains)
33+
3234
zone_id = local.create_hosted_zone ? aws_route53_zone.hosted_zone[0].zone_id : data.aws_route53_zone.hosted_zone[0].zone_id
33-
name = var.domain_name
3435
type = "A"
36+
name = local.domains[count.index]
3537

3638
alias {
3739
name = aws_cloudfront_distribution.static_site.domain_name

static-site/variables.tf

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ variable "hosted_zone" {
1010
default = ""
1111
}
1212

13-
variable "domain_name" {
14-
type = string
15-
description = "The custom domain name for your CloudFront distribution"
13+
variable "domains" {
14+
type = list(string)
15+
description = "List of custom domain names for your CloudFront distribution. The first domain specified will be classed as the primary domain (used as S3 bucket name, Route53 hosted zone name etc.)"
16+
validation {
17+
condition = length(var.domains) > 0
18+
error_message = "domains requires at least one domain to be specified"
19+
}
1620
}
1721

1822
variable "role_name" {

0 commit comments

Comments
 (0)