From c533fa2191509090b6d45219b06ec68faa098ca2 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 1 Nov 2025 19:02:39 -0500 Subject: [PATCH 1/3] multiregion S3 module setup --- multiregion-s3/README.md | 2 + multiregion-s3/main.tf | 143 ++++++++++++++++++++++++++++++++++++ multiregion-s3/variables.tf | 28 +++++++ 3 files changed, 173 insertions(+) create mode 100644 multiregion-s3/README.md create mode 100644 multiregion-s3/main.tf create mode 100644 multiregion-s3/variables.tf diff --git a/multiregion-s3/README.md b/multiregion-s3/README.md new file mode 100644 index 0000000..8c460a5 --- /dev/null +++ b/multiregion-s3/README.md @@ -0,0 +1,2 @@ +# Multiregion S3 bucket +Creates a bi-directionally replicated S3 bucket between 2 regions with the same suffix. \ No newline at end of file diff --git a/multiregion-s3/main.tf b/multiregion-s3/main.tf new file mode 100644 index 0000000..db66c4a --- /dev/null +++ b/multiregion-s3/main.tf @@ -0,0 +1,143 @@ +# S3 Buckets +resource "aws_s3_bucket" "buckets" { + for_each = toset([var.Region1, var.Region2]) + region = each.value + bucket = "${var.BucketPrefix}-${each.value}" +} + +# Enable versioning (required for replication) +resource "aws_s3_bucket_versioning" "versioning" { + for_each = toset([var.Region1, var.Region2]) + region = each.value + bucket = aws_s3_bucket.buckets[each.value].id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_iam_role" "replication" { + name = "${var.BucketPrefix}-replication-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "s3.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_policy" "replication" { + name = "${var.BucketPrefix}-replication-policy" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:GetReplicationConfiguration", + "s3:ListBucket" + ] + Effect = "Allow" + Resource = [ + for bucket in aws_s3_bucket.buckets : bucket.arn + ] + }, + { + Action = [ + "s3:GetObjectVersionForReplication", + "s3:GetObjectVersionAcl", + "s3:GetObjectVersionTagging" + ] + Effect = "Allow" + Resource = [ + for bucket in aws_s3_bucket.buckets : "${bucket.arn}/*" + ] + }, + { + Action = [ + "s3:ReplicateObject", + "s3:ReplicateDelete", + "s3:ReplicateTags" + ] + Effect = "Allow" + Resource = [ + for bucket in aws_s3_bucket.buckets : "${bucket.arn}/*" + ] + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "replication" { + role = aws_iam_role.replication.name + policy_arn = aws_iam_policy.replication.arn +} + +# Replication configuration: Region1 -> Region2 +resource "aws_s3_bucket_replication_configuration" "region1_to_region2" { + region = var.Region1 + role = aws_iam_role.replication.arn + bucket = aws_s3_bucket.buckets[var.Region1].id + + rule { + id = "replicate-to-${var.Region2}" + status = "Enabled" + priority = 1 + + filter {} + + destination { + bucket = aws_s3_bucket.buckets[var.Region2].arn + storage_class = "STANDARD" + } + + delete_marker_replication { + status = "Enabled" + } + } + + depends_on = [aws_s3_bucket_versioning.versioning] +} + +# Replication configuration: Region2 -> Region1 +resource "aws_s3_bucket_replication_configuration" "region2_to_region1" { + region = var.Region2 + role = aws_iam_role.replication.arn + bucket = aws_s3_bucket.buckets[var.Region2].id + + rule { + id = "replicate-to-${var.Region1}" + status = "Enabled" + priority = 1 + + filter {} + + destination { + bucket = aws_s3_bucket.buckets[var.Region1].arn + storage_class = "STANDARD" + } + + delete_marker_replication { + status = "Enabled" + } + } + + depends_on = [aws_s3_bucket_versioning.versioning] +} + +output "buckets_info" { + description = "Map of bucket information by region" + value = { + for region, bucket in aws_s3_bucket.buckets : region => { + arn = bucket.arn + bucket_regional_domain_name = bucket.bucket_regional_domain_name + bucket_domain_name = bucket.bucket_domain_name + } + } +} diff --git a/multiregion-s3/variables.tf b/multiregion-s3/variables.tf new file mode 100644 index 0000000..8f5284b --- /dev/null +++ b/multiregion-s3/variables.tf @@ -0,0 +1,28 @@ +data "aws_regions" "current" { + all_regions = true +} + +variable "Region1" { + type = string + description = "Region for bucket 1" + + validation { + condition = contains(data.aws_regions.current.names, var.PrimaryRegion) + error_message = "Region1 must be a valid AWS region. Available regions: ${join(", ", data.aws_regions.current.names)}" + } +} + +variable "Region2" { + type = string + description = "Region for bucket 2" + + validation { + condition = contains(data.aws_regions.current.names, var.PrimaryRegion) + error_message = "Region1 must be a valid AWS region. Available regions: ${join(", ", data.aws_regions.current.names)}" + } +} + +variable "BucketPrefix" { + type = string + description = "Prefix for the bucket. The created bucket names will be in the form of BucketPrefix-Region." +} From 99de4c350d1e35931f94499e0c06cbf29d0d5b8a Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 1 Nov 2025 19:11:01 -0500 Subject: [PATCH 2/3] Also output bucket ID --- multiregion-s3/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/multiregion-s3/main.tf b/multiregion-s3/main.tf index db66c4a..9a11fcf 100644 --- a/multiregion-s3/main.tf +++ b/multiregion-s3/main.tf @@ -138,6 +138,7 @@ output "buckets_info" { arn = bucket.arn bucket_regional_domain_name = bucket.bucket_regional_domain_name bucket_domain_name = bucket.bucket_domain_name + id = bucket.id } } } From 976e9eb8f8a29746b02d9e85d78d2952e3548bf9 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 1 Nov 2025 19:23:47 -0500 Subject: [PATCH 3/3] Fix --- multiregion-s3/variables.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multiregion-s3/variables.tf b/multiregion-s3/variables.tf index 8f5284b..81b6498 100644 --- a/multiregion-s3/variables.tf +++ b/multiregion-s3/variables.tf @@ -7,7 +7,7 @@ variable "Region1" { description = "Region for bucket 1" validation { - condition = contains(data.aws_regions.current.names, var.PrimaryRegion) + condition = contains(data.aws_regions.current.names, var.Region1) error_message = "Region1 must be a valid AWS region. Available regions: ${join(", ", data.aws_regions.current.names)}" } } @@ -17,7 +17,7 @@ variable "Region2" { description = "Region for bucket 2" validation { - condition = contains(data.aws_regions.current.names, var.PrimaryRegion) + condition = contains(data.aws_regions.current.names, var.Region2) error_message = "Region1 must be a valid AWS region. Available regions: ${join(", ", data.aws_regions.current.names)}" } }