Skip to content

Commit e99a014

Browse files
liyin00liyin
and
liyin
authored
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 <[email protected]>
1 parent 66b6a1a commit e99a014

File tree

4 files changed

+218
-8
lines changed

4 files changed

+218
-8
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ module "s3-generic" {
5252
|------|-------------|------|---------|:--------:|
5353
| <a name="input_force_destroy"></a> [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 |
5454
| <a name="input_path"></a> [path](#input\_path) | Desired path for the IAM user | `string` | `"/"` | no |
55-
| <a name="input_s3_buckets"></a> [s3\_buckets](#input\_s3\_buckets) | A map of bucket names to an object describing the S3 bucket settings for the bucket. | <pre>map(object({<br/> bucket = string<br/> permissions_boundary = string<br/> region = string<br/> acl = optional(string)<br/> log_bucket_for_s3 = optional(string)<br/> policies = list(string)<br/> server_side_encryption_configuration = any<br/> cors_configuration = optional(<br/> list(<br/> object({<br/> allowed_methods = list(string)<br/> allowed_origins = list(string)<br/> allowed_headers = optional(list(string))<br/> expose_headers = optional(list(string))<br/> max_age_seconds = optional(number)<br/> id = optional(string)<br/> })<br/> )<br/> )<br/> }))</pre> | <pre>{<br/> "main": {<br/> "bucket": "",<br/> "log_bucket_for_s3": "",<br/> "permissions_boundary": "",<br/> "policies": [],<br/> "region": "ap-southeast-1",<br/> "server_side_encryption_configuration": {<br/> "rule": {<br/> "apply_server_side_encryption_by_default": {<br/> "sse_algorithm": "AES256"<br/> }<br/> }<br/> }<br/> }<br/>}</pre> | no |
55+
| <a name="input_s3_buckets"></a> [s3\_buckets](#input\_s3\_buckets) | A map of bucket names to an object describing the S3 bucket settings for the bucket. | <pre>map(object({ </br> bucket = string </br> permissions_boundary = string </br> region = string </br> acl = optional(string) </br> log_bucket_for_s3 = optional(string) </br> policies = list(string) </br> server_side_encryption_configuration = any </br> cors_configuration = optional( </br> list( </br> object({ </br> allowed_methods = list(string) </br> allowed_origins = list(string) </br> allowed_headers = optional(list(string)) </br> expose_headers = optional(list(string)) </br> max_age_seconds = optional(number) </br> id = optional(string) </br> }) </br> ) </br> ) </br> lifecycle_rules = optional(list(object({ </br> id = optional(string) </br> enabled = optional(bool, true) </br> filter = optional(object({ </br> prefix = optional(string) </br> object_size_greater_than = optional(number) </br> object_size_less_than = optional(number) </br> tags = optional(map(string)) </br> })) </br> transition = optional(list(object({ </br> days = optional(number) </br> date = optional(string) </br> storage_class = string </br> }))) </br> }))) </br> })) </br></pre> | no |
5656
| <a name="input_tags"></a> [tags](#input\_tags) | (Optional) A mapping of tags to assign to the bucket. | `map(string)` | `{}` | no |
5757

5858
## Outputs

examples/full/main.tf

+43
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,49 @@ module "s3-generic" {
3535
}
3636
}
3737
}
38+
lifecycle_rules = [
39+
{
40+
id = "backup-lifecycle-rule"
41+
enabled = true
42+
filter = {
43+
object_size_greater_than = 0
44+
}
45+
transition = [
46+
{
47+
days = 30
48+
storage_class = "STANDARD_IA"
49+
},
50+
{
51+
days = 60
52+
storage_class = "GLACIER"
53+
},
54+
{
55+
days = 150
56+
storage_class = "DEEP_ARCHIVE"
57+
}
58+
]
59+
noncurrent_version_transition = [
60+
{
61+
noncurrent_days = 30
62+
storage_class = "STANDARD_IA"
63+
},
64+
{
65+
noncurrent_days = 60
66+
storage_class = "GLACIER"
67+
},
68+
{
69+
noncurrent_days = 150
70+
storage_class = "DEEP_ARCHIVE"
71+
}
72+
]
73+
expiration = {
74+
days = 183
75+
}
76+
noncurrent_version_expiration = {
77+
noncurrent_days = 151
78+
}
79+
}
80+
]
3881
}
3982
}
4083
}

main.tf

+152
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
data "aws_caller_identity" "current" {}
12

23
data "aws_iam_policy_document" "main" {
34
for_each = var.s3_buckets
@@ -208,3 +209,154 @@ resource "aws_iam_role_policy" "main" {
208209
]
209210
})
210211
}
212+
213+
###################################################################
214+
# Lifecycle Config
215+
###################################################################
216+
217+
resource "aws_s3_bucket_lifecycle_configuration" "main" {
218+
# Only create lifecycle rules for buckets that have them defined
219+
for_each = {
220+
for key, value in var.s3_buckets : key => value
221+
if try(length(value.lifecycle_rules), 0) > 0
222+
}
223+
224+
bucket = each.value.bucket
225+
expected_bucket_owner = data.aws_caller_identity.current.account_id
226+
227+
dynamic "rule" {
228+
for_each = each.value.lifecycle_rules
229+
230+
content {
231+
id = coalesce(try(rule.value.id, null), "rule-${rule.key}")
232+
status = coalesce(
233+
try(rule.value.enabled ? "Enabled" : "Disabled", null),
234+
try(tobool(rule.value.status) ? "Enabled" : "Disabled", null),
235+
try(title(lower(rule.value.status)), null),
236+
"Enabled"
237+
)
238+
239+
# Abort incomplete multipart uploads
240+
dynamic "abort_incomplete_multipart_upload" {
241+
for_each = try(rule.value.abort_incomplete_multipart_upload_days != null ? [rule.value.abort_incomplete_multipart_upload_days] : [], [])
242+
243+
content {
244+
days_after_initiation = abort_incomplete_multipart_upload.value
245+
}
246+
}
247+
248+
# Object expiration rules
249+
dynamic "expiration" {
250+
for_each = try(rule.value.expiration != null ? [rule.value.expiration] : [], [])
251+
252+
content {
253+
date = try(expiration.value.date, null)
254+
days = try(expiration.value.days, null)
255+
expired_object_delete_marker = try(expiration.value.expired_object_delete_marker, null)
256+
}
257+
}
258+
259+
# Object transition rules
260+
dynamic "transition" {
261+
for_each = try(rule.value.transition != null ? rule.value.transition : [], [])
262+
263+
content {
264+
date = try(transition.value.date, null)
265+
days = try(transition.value.days, null)
266+
storage_class = transition.value.storage_class
267+
}
268+
}
269+
270+
# Non-current version expiration
271+
dynamic "noncurrent_version_expiration" {
272+
for_each = try(rule.value.noncurrent_version_expiration != null ? [rule.value.noncurrent_version_expiration] : [], [])
273+
274+
content {
275+
newer_noncurrent_versions = try(noncurrent_version_expiration.value.newer_noncurrent_versions, null)
276+
noncurrent_days = coalesce(
277+
try(noncurrent_version_expiration.value.days, null),
278+
try(noncurrent_version_expiration.value.noncurrent_days, null)
279+
)
280+
}
281+
}
282+
283+
# Non-current version transition
284+
dynamic "noncurrent_version_transition" {
285+
for_each = try(rule.value.noncurrent_version_transition != null ? rule.value.noncurrent_version_transition : [], [])
286+
287+
content {
288+
newer_noncurrent_versions = try(noncurrent_version_transition.value.newer_noncurrent_versions, null)
289+
noncurrent_days = coalesce(
290+
try(noncurrent_version_transition.value.days, null),
291+
try(noncurrent_version_transition.value.noncurrent_days, null)
292+
)
293+
storage_class = noncurrent_version_transition.value.storage_class
294+
}
295+
}
296+
297+
# Empty filter
298+
dynamic "filter" {
299+
for_each = length(try(flatten([rule.value.filter]), [])) == 0 ? [true] : []
300+
301+
content {
302+
}
303+
}
304+
305+
# Single condition filter (no "and" block needed)
306+
dynamic "filter" {
307+
for_each = [for v in try(flatten([rule.value.filter]), []) : v if(
308+
length(compact([
309+
try(v.prefix, null),
310+
try(v.object_size_greater_than, null),
311+
try(v.object_size_less_than, null)
312+
])) <= 1 &&
313+
length(try(flatten([v.tags, v.tag]), [])) <= 1
314+
)]
315+
316+
content {
317+
object_size_greater_than = try(filter.value.object_size_greater_than, null)
318+
object_size_less_than = try(filter.value.object_size_less_than, null)
319+
prefix = try(filter.value.prefix, null)
320+
321+
dynamic "tag" {
322+
for_each = try(flatten([filter.value.tags, filter.value.tag]), [])
323+
content {
324+
key = try(tag.value.key, null)
325+
value = try(tag.value.value, null)
326+
}
327+
}
328+
}
329+
}
330+
331+
# Multiple conditions filter (requires "and" block)
332+
dynamic "filter" {
333+
for_each = [for v in try(flatten([rule.value.filter]), []) : v if(
334+
length(compact([
335+
try(v.prefix, null),
336+
try(v.object_size_greater_than, null),
337+
try(v.object_size_less_than, null)
338+
])) > 1 ||
339+
length(try(flatten([v.tags, v.tag]), [])) > 1
340+
)]
341+
342+
content {
343+
and {
344+
object_size_greater_than = try(filter.value.object_size_greater_than, null)
345+
object_size_less_than = try(filter.value.object_size_less_than, null)
346+
prefix = try(filter.value.prefix, null)
347+
tags = try(filter.value.tags, filter.value.tag, null)
348+
}
349+
}
350+
}
351+
}
352+
}
353+
354+
depends_on = [aws_s3_bucket_versioning.main]
355+
356+
lifecycle {
357+
precondition {
358+
condition = can(aws_s3_bucket_versioning.main[each.key].versioning_configuration[0].status == "Enabled")
359+
error_message = "S3 bucket versioning must be enabled to use lifecycle rules with version-specific actions."
360+
}
361+
}
362+
}

variables.tf

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
variable "s3_buckets" {
22
description = "A map of bucket names to an object describing the S3 bucket settings for the bucket."
3-
type = map(object({
3+
type = map(object({
44
bucket = string
55
permissions_boundary = string
66
region = string
77
acl = optional(string)
88
log_bucket_for_s3 = optional(string)
99
policies = list(string)
1010
server_side_encryption_configuration = any
11-
cors_configuration = optional(
11+
cors_configuration = optional(
1212
list(
1313
object({
1414
allowed_methods = list(string)
@@ -20,15 +20,30 @@ variable "s3_buckets" {
2020
})
2121
)
2222
)
23+
lifecycle_rules = optional(list(object({
24+
id = optional(string)
25+
enabled = optional(bool, true)
26+
filter = optional(object({
27+
prefix = optional(string)
28+
object_size_greater_than = optional(number)
29+
object_size_less_than = optional(number)
30+
tags = optional(map(string))
31+
}))
32+
transition = optional(list(object({
33+
days = optional(number)
34+
date = optional(string)
35+
storage_class = string
36+
})))
37+
})))
2338
}))
2439

2540
default = {
2641
main = {
27-
bucket = ""
28-
region = "ap-southeast-1"
29-
permissions_boundary = ""
30-
log_bucket_for_s3 = ""
31-
policies = []
42+
bucket = ""
43+
region = "ap-southeast-1"
44+
permissions_boundary = ""
45+
log_bucket_for_s3 = ""
46+
policies = []
3247
server_side_encryption_configuration = {
3348
rule = {
3449
apply_server_side_encryption_by_default = {

0 commit comments

Comments
 (0)