Skip to content

Commit e4ce2f5

Browse files
authored
Host UI in all AWS regions we're in (#360)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Frontend now deploys to multiple regions with region-aware CDN origins, per-region origin groups and automated failover for faster regional access and improved resilience. * Lambda backends now have per-region origins to reduce latency and improve availability. * **Chores** * Per-region provisioning added for storage, lifecycle, uploads, bucket policy application and cache configuration to enable targeted invalidations and better geographic availability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 28f5550 commit e4ce2f5

File tree

1 file changed

+118
-38
lines changed

1 file changed

+118
-38
lines changed

terraform/modules/frontend/main.tf

Lines changed: 118 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
locals {
2+
all_regions = keys(var.CoreSlowLambdaHost)
3+
}
4+
5+
data "aws_caller_identity" "current" {}
6+
17
resource "aws_s3_bucket" "frontend" {
2-
bucket = "${var.BucketPrefix}-${var.ProjectId}"
8+
region = each.key
9+
for_each = toset(local.all_regions)
10+
bucket = "${data.aws_caller_identity.current.account_id}-${var.ProjectId}-${each.key}"
311
}
412

513
resource "aws_s3_bucket_lifecycle_configuration" "frontend" {
6-
bucket = aws_s3_bucket.frontend.id
14+
for_each = toset(local.all_regions)
15+
region = each.key
16+
bucket = aws_s3_bucket.frontend[each.key].id
717

818
rule {
919
id = "AbortIncompleteMultipartUploads"
@@ -41,13 +51,16 @@ data "archive_file" "ui" {
4151
source_dir = "${path.module}/../../../dist_ui/"
4252
output_path = "/tmp/ui_archive.zip"
4353
}
54+
4455
resource "null_resource" "upload_frontend" {
56+
for_each = toset(local.all_regions)
57+
4558
triggers = {
4659
ui_bucket_sha = data.archive_file.ui.output_sha
4760
}
4861

4962
provisioner "local-exec" {
50-
command = "aws s3 sync ${data.archive_file.ui.source_dir} s3://${aws_s3_bucket.frontend.id} --delete"
63+
command = "aws s3 sync ${data.archive_file.ui.source_dir} s3://${aws_s3_bucket.frontend[each.key].id} --region ${each.key} --delete"
5164
}
5265
}
5366

@@ -120,10 +133,35 @@ resource "aws_cloudfront_cache_policy" "no_cache" {
120133

121134
resource "aws_cloudfront_distribution" "app_cloudfront_distribution" {
122135
http_version = "http2and3"
123-
origin {
124-
origin_id = "S3Bucket"
125-
origin_access_control_id = aws_cloudfront_origin_access_control.frontend_oac.id
126-
domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
136+
137+
# Dynamic origins for each region's S3 bucket
138+
dynamic "origin" {
139+
for_each = local.all_regions
140+
content {
141+
origin_id = "S3Bucket-${origin.value}"
142+
origin_access_control_id = aws_cloudfront_origin_access_control.frontend_oac.id
143+
domain_name = aws_s3_bucket.frontend[origin.value].bucket_regional_domain_name
144+
}
145+
}
146+
147+
# Origin group for S3 buckets with failover
148+
origin_group {
149+
origin_id = "S3BucketGroup"
150+
151+
failover_criteria {
152+
status_codes = [403, 404, 500, 502, 503, 504]
153+
}
154+
155+
member {
156+
origin_id = "S3Bucket-${var.CurrentActiveRegion}"
157+
}
158+
159+
dynamic "member" {
160+
for_each = [for region in local.all_regions : region if region != var.CurrentActiveRegion]
161+
content {
162+
origin_id = "S3Bucket-${member.value}"
163+
}
164+
}
127165
}
128166

129167
# Dynamic origins for each region's Lambda function
@@ -161,7 +199,7 @@ resource "aws_cloudfront_distribution" "app_cloudfront_distribution" {
161199
is_ipv6_enabled = true
162200
default_cache_behavior {
163201
compress = true
164-
target_origin_id = "S3Bucket"
202+
target_origin_id = "S3BucketGroup"
165203
viewer_protocol_policy = "redirect-to-https"
166204
allowed_methods = ["GET", "HEAD"]
167205
cached_methods = ["GET", "HEAD"]
@@ -329,41 +367,83 @@ function handler(event) {
329367
EOT
330368
}
331369

332-
resource "aws_s3_bucket_policy" "frontend_bucket_policy" {
333-
bucket = aws_s3_bucket.frontend.id
334-
policy = jsonencode(({
335-
Version = "2012-10-17"
336-
Statement = [
337-
{
338-
Effect = "Allow",
339-
Principal = {
340-
Service = "cloudfront.amazonaws.com"
341-
},
342-
Action = "s3:GetObject",
343-
Resource = "${aws_s3_bucket.frontend.arn}/*"
344-
Condition = {
345-
StringEquals = {
346-
"AWS:SourceArn" = aws_cloudfront_distribution.app_cloudfront_distribution.arn
370+
resource "null_resource" "s3_bucket_policy" {
371+
for_each = toset(local.all_regions)
372+
373+
triggers = {
374+
bucket_id = aws_s3_bucket.frontend[each.key].id
375+
distribution_arn = aws_cloudfront_distribution.app_cloudfront_distribution.arn
376+
policy_hash = md5(jsonencode({
377+
Version = "2012-10-17"
378+
Statement = [
379+
{
380+
Effect = "Allow",
381+
Principal = {
382+
Service = "cloudfront.amazonaws.com"
383+
},
384+
Action = "s3:GetObject",
385+
Resource = "${aws_s3_bucket.frontend[each.key].arn}/*"
386+
Condition = {
387+
StringEquals = {
388+
"AWS:SourceArn" = aws_cloudfront_distribution.app_cloudfront_distribution.arn
389+
}
347390
}
348-
}
349-
},
350-
{
351-
Effect = "Allow",
352-
Principal = {
353-
Service = "cloudfront.amazonaws.com"
354391
},
355-
Action = "s3:ListBucket",
356-
Resource = aws_s3_bucket.frontend.arn
357-
Condition = {
358-
StringEquals = {
359-
"AWS:SourceArn" = aws_cloudfront_distribution.app_cloudfront_distribution.arn
392+
{
393+
Effect = "Allow",
394+
Principal = {
395+
Service = "cloudfront.amazonaws.com"
396+
},
397+
Action = "s3:ListBucket",
398+
Resource = aws_s3_bucket.frontend[each.key].arn
399+
Condition = {
400+
StringEquals = {
401+
"AWS:SourceArn" = aws_cloudfront_distribution.app_cloudfront_distribution.arn
402+
}
360403
}
361404
}
362-
}
363-
]
364-
365-
}))
405+
]
406+
}))
407+
}
366408

409+
provisioner "local-exec" {
410+
command = <<-EOT
411+
aws s3api put-bucket-policy \
412+
--bucket ${aws_s3_bucket.frontend[each.key].id} \
413+
--region ${each.key} \
414+
--policy '{
415+
"Version": "2012-10-17",
416+
"Statement": [
417+
{
418+
"Effect": "Allow",
419+
"Principal": {
420+
"Service": "cloudfront.amazonaws.com"
421+
},
422+
"Action": "s3:GetObject",
423+
"Resource": "${aws_s3_bucket.frontend[each.key].arn}/*",
424+
"Condition": {
425+
"StringEquals": {
426+
"AWS:SourceArn": "${aws_cloudfront_distribution.app_cloudfront_distribution.arn}"
427+
}
428+
}
429+
},
430+
{
431+
"Effect": "Allow",
432+
"Principal": {
433+
"Service": "cloudfront.amazonaws.com"
434+
},
435+
"Action": "s3:ListBucket",
436+
"Resource": "${aws_s3_bucket.frontend[each.key].arn}",
437+
"Condition": {
438+
"StringEquals": {
439+
"AWS:SourceArn": "${aws_cloudfront_distribution.app_cloudfront_distribution.arn}"
440+
}
441+
}
442+
}
443+
]
444+
}'
445+
EOT
446+
}
367447
}
368448

369449
resource "aws_cloudfront_distribution" "linkry_cloudfront_distribution" {

0 commit comments

Comments
 (0)