From e99a0141ff508a5a9f60cf59f5a5242a86ece4c1 Mon Sep 17 00:00:00 2001 From: Li Yin <73151500+liyin00@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:27:14 +0800 Subject: [PATCH] Chore/000 add lifecycle config (#15) * add lifecycle config * amend ref names * amend filtering * amend filtering * add on to readme * revert object ownership change * amend README for lifecycle config --------- Co-authored-by: liyin --- README.md | 2 +- examples/full/main.tf | 43 ++++++++++++ main.tf | 152 ++++++++++++++++++++++++++++++++++++++++++ variables.tf | 29 ++++++-- 4 files changed, 218 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 97b2389..1f38317 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ module "s3-generic" { |------|-------------|------|---------|:--------:| | [force\_destroy](#input\_force\_destroy) | When destroying this user, destroy even if it has non-Terraform-managed IAM access keys, login profile or MFA devices. Without force\_destroy a user with non-Terraform-managed access keys and login profile will fail to be destroyed. | `bool` | `false` | no | | [path](#input\_path) | Desired path for the IAM user | `string` | `"/"` | no | -| [s3\_buckets](#input\_s3\_buckets) | A map of bucket names to an object describing the S3 bucket settings for the bucket. |
map(object({
bucket = string
permissions_boundary = string
region = string
acl = optional(string)
log_bucket_for_s3 = optional(string)
policies = list(string)
server_side_encryption_configuration = any
cors_configuration = optional(
list(
object({
allowed_methods = list(string)
allowed_origins = list(string)
allowed_headers = optional(list(string))
expose_headers = optional(list(string))
max_age_seconds = optional(number)
id = optional(string)
})
)
)
}))
|
{
"main": {
"bucket": "",
"log_bucket_for_s3": "",
"permissions_boundary": "",
"policies": [],
"region": "ap-southeast-1",
"server_side_encryption_configuration": {
"rule": {
"apply_server_side_encryption_by_default": {
"sse_algorithm": "AES256"
}
}
}
}
}
| no | +| [s3\_buckets](#input\_s3\_buckets) | A map of bucket names to an object describing the S3 bucket settings for the bucket. |
map(object({ 
bucket = string
permissions_boundary = string
region = string
acl = optional(string)
log_bucket_for_s3 = optional(string)
policies = list(string)
server_side_encryption_configuration = any
cors_configuration = optional(
list(
object({
allowed_methods = list(string)
allowed_origins = list(string)
allowed_headers = optional(list(string))
expose_headers = optional(list(string))
max_age_seconds = optional(number)
id = optional(string)
})
)
)
lifecycle_rules = optional(list(object({
id = optional(string)
enabled = optional(bool, true)
filter = optional(object({
prefix = optional(string)
object_size_greater_than = optional(number)
object_size_less_than = optional(number)
tags = optional(map(string))
}))
transition = optional(list(object({
days = optional(number)
date = optional(string)
storage_class = string
})))
})))
}))
| no | | [tags](#input\_tags) | (Optional) A mapping of tags to assign to the bucket. | `map(string)` | `{}` | no | ## Outputs diff --git a/examples/full/main.tf b/examples/full/main.tf index bc1601a..d2c75fe 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -35,6 +35,49 @@ module "s3-generic" { } } } + lifecycle_rules = [ + { + id = "backup-lifecycle-rule" + enabled = true + filter = { + object_size_greater_than = 0 + } + transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + { + days = 60 + storage_class = "GLACIER" + }, + { + days = 150 + storage_class = "DEEP_ARCHIVE" + } + ] + noncurrent_version_transition = [ + { + noncurrent_days = 30 + storage_class = "STANDARD_IA" + }, + { + noncurrent_days = 60 + storage_class = "GLACIER" + }, + { + noncurrent_days = 150 + storage_class = "DEEP_ARCHIVE" + } + ] + expiration = { + days = 183 + } + noncurrent_version_expiration = { + noncurrent_days = 151 + } + } + ] } } } diff --git a/main.tf b/main.tf index 03a0e70..94bff48 100644 --- a/main.tf +++ b/main.tf @@ -1,3 +1,4 @@ +data "aws_caller_identity" "current" {} data "aws_iam_policy_document" "main" { for_each = var.s3_buckets @@ -208,3 +209,154 @@ resource "aws_iam_role_policy" "main" { ] }) } + +################################################################### +# Lifecycle Config +################################################################### + +resource "aws_s3_bucket_lifecycle_configuration" "main" { + # Only create lifecycle rules for buckets that have them defined + for_each = { + for key, value in var.s3_buckets : key => value + if try(length(value.lifecycle_rules), 0) > 0 + } + + bucket = each.value.bucket + expected_bucket_owner = data.aws_caller_identity.current.account_id + + dynamic "rule" { + for_each = each.value.lifecycle_rules + + content { + id = coalesce(try(rule.value.id, null), "rule-${rule.key}") + status = coalesce( + try(rule.value.enabled ? "Enabled" : "Disabled", null), + try(tobool(rule.value.status) ? "Enabled" : "Disabled", null), + try(title(lower(rule.value.status)), null), + "Enabled" + ) + + # Abort incomplete multipart uploads + dynamic "abort_incomplete_multipart_upload" { + for_each = try(rule.value.abort_incomplete_multipart_upload_days != null ? [rule.value.abort_incomplete_multipart_upload_days] : [], []) + + content { + days_after_initiation = abort_incomplete_multipart_upload.value + } + } + + # Object expiration rules + dynamic "expiration" { + for_each = try(rule.value.expiration != null ? [rule.value.expiration] : [], []) + + content { + date = try(expiration.value.date, null) + days = try(expiration.value.days, null) + expired_object_delete_marker = try(expiration.value.expired_object_delete_marker, null) + } + } + + # Object transition rules + dynamic "transition" { + for_each = try(rule.value.transition != null ? rule.value.transition : [], []) + + content { + date = try(transition.value.date, null) + days = try(transition.value.days, null) + storage_class = transition.value.storage_class + } + } + + # Non-current version expiration + dynamic "noncurrent_version_expiration" { + for_each = try(rule.value.noncurrent_version_expiration != null ? [rule.value.noncurrent_version_expiration] : [], []) + + content { + newer_noncurrent_versions = try(noncurrent_version_expiration.value.newer_noncurrent_versions, null) + noncurrent_days = coalesce( + try(noncurrent_version_expiration.value.days, null), + try(noncurrent_version_expiration.value.noncurrent_days, null) + ) + } + } + + # Non-current version transition + dynamic "noncurrent_version_transition" { + for_each = try(rule.value.noncurrent_version_transition != null ? rule.value.noncurrent_version_transition : [], []) + + content { + newer_noncurrent_versions = try(noncurrent_version_transition.value.newer_noncurrent_versions, null) + noncurrent_days = coalesce( + try(noncurrent_version_transition.value.days, null), + try(noncurrent_version_transition.value.noncurrent_days, null) + ) + storage_class = noncurrent_version_transition.value.storage_class + } + } + + # Empty filter + dynamic "filter" { + for_each = length(try(flatten([rule.value.filter]), [])) == 0 ? [true] : [] + + content { + } + } + + # Single condition filter (no "and" block needed) + dynamic "filter" { + for_each = [for v in try(flatten([rule.value.filter]), []) : v if( + length(compact([ + try(v.prefix, null), + try(v.object_size_greater_than, null), + try(v.object_size_less_than, null) + ])) <= 1 && + length(try(flatten([v.tags, v.tag]), [])) <= 1 + )] + + content { + object_size_greater_than = try(filter.value.object_size_greater_than, null) + object_size_less_than = try(filter.value.object_size_less_than, null) + prefix = try(filter.value.prefix, null) + + dynamic "tag" { + for_each = try(flatten([filter.value.tags, filter.value.tag]), []) + content { + key = try(tag.value.key, null) + value = try(tag.value.value, null) + } + } + } + } + + # Multiple conditions filter (requires "and" block) + dynamic "filter" { + for_each = [for v in try(flatten([rule.value.filter]), []) : v if( + length(compact([ + try(v.prefix, null), + try(v.object_size_greater_than, null), + try(v.object_size_less_than, null) + ])) > 1 || + length(try(flatten([v.tags, v.tag]), [])) > 1 + )] + + content { + and { + object_size_greater_than = try(filter.value.object_size_greater_than, null) + object_size_less_than = try(filter.value.object_size_less_than, null) + prefix = try(filter.value.prefix, null) + tags = try(filter.value.tags, filter.value.tag, null) + } + } + } + } + } + + depends_on = [aws_s3_bucket_versioning.main] + + lifecycle { + precondition { + condition = can(aws_s3_bucket_versioning.main[each.key].versioning_configuration[0].status == "Enabled") + error_message = "S3 bucket versioning must be enabled to use lifecycle rules with version-specific actions." + } + } +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index f6381f3..1949708 100644 --- a/variables.tf +++ b/variables.tf @@ -1,6 +1,6 @@ variable "s3_buckets" { description = "A map of bucket names to an object describing the S3 bucket settings for the bucket." - type = map(object({ + type = map(object({ bucket = string permissions_boundary = string region = string @@ -8,7 +8,7 @@ variable "s3_buckets" { log_bucket_for_s3 = optional(string) policies = list(string) server_side_encryption_configuration = any - cors_configuration = optional( + cors_configuration = optional( list( object({ allowed_methods = list(string) @@ -20,15 +20,30 @@ variable "s3_buckets" { }) ) ) + lifecycle_rules = optional(list(object({ + id = optional(string) + enabled = optional(bool, true) + filter = optional(object({ + prefix = optional(string) + object_size_greater_than = optional(number) + object_size_less_than = optional(number) + tags = optional(map(string)) + })) + transition = optional(list(object({ + days = optional(number) + date = optional(string) + storage_class = string + }))) + }))) })) default = { main = { - bucket = "" - region = "ap-southeast-1" - permissions_boundary = "" - log_bucket_for_s3 = "" - policies = [] + bucket = "" + region = "ap-southeast-1" + permissions_boundary = "" + log_bucket_for_s3 = "" + policies = [] server_side_encryption_configuration = { rule = { apply_server_side_encryption_by_default = {