From 5eb325292f9e41fa3b08e084fe393b88fe066bab Mon Sep 17 00:00:00 2001 From: Steven Gorrell Date: Thu, 1 Dec 2022 14:55:01 -0600 Subject: [PATCH] Fix deprecations, new features, and update tests - Object lock uses new resource - Website now supports full complex configuration - Website now supports routing rules and scales to up to 100 rules - Added full website test - Lifecycle updated to support multiple complex rules - Lifecycle now supports intelligent tiering - Lifecycle now supports metric configuration - Lifecycle now supports multiple transition rules --- README.md | 53 +++--- examples/s3.tf | 285 +++++++++++++++++++++++------- main.tf | 421 +++++++++++++++++++++++--------------------- tests/test1/main.tf | 47 +++-- tests/test2/main.tf | 66 ++++--- tests/test3/main.tf | 196 +++++++++++++++------ tests/test4/main.tf | 45 +++-- variables.tf | 81 +++------ 8 files changed, 724 insertions(+), 470 deletions(-) diff --git a/README.md b/README.md index fe742e9..32c39b0 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,22 @@ It will not do s3 origin, which is in another module. ## Basic Usage ```HCL -module "s3" { - source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.3" - - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" +module "s3_basic" { + source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" + + bucket_logging = false + bucket_acl = "private" + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + lifecycle_enabled = true + lifecycle_rule = [ + { + id = "Default MPU Cleanup Rule." + enabled = true + abort_incomplete_multipart_upload_days = 7 + } + ] tags = { RightSaid = "Fred" @@ -68,19 +66,22 @@ No Modules. | Name | |------| +| [aws_canonical_user_id](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/data-sources/canonical_user_id) | | [aws_s3_bucket](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket) | | [aws_s3_bucket_acl](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_acl) | | [aws_s3_bucket_cors_configuration](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_cors_configuration) | +| [aws_s3_bucket_lifecycle_configuration](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_lifecycle_configuration) | | [aws_s3_bucket_logging](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_logging) | +| [aws_s3_bucket_object_lock_configuration](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_object_lock_configuration) | | [aws_s3_bucket_public_access_block](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_public_access_block) | | [aws_s3_bucket_server_side_encryption_configuration](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_server_side_encryption_configuration) | | [aws_s3_bucket_versioning](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_versioning) | +| [aws_s3_bucket_website_configuration](https://registry.terraform.io/providers/hashicorp/aws/3.0/docs/resources/s3_bucket_website_configuration) | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| abort\_incomplete\_multipart\_upload\_days | Abort Incomplete Multipart Upload Days i.e. 7 \| 0 | `number` | `7` | no | | block\_public\_access | Block various forms of public access on a per bucket level | `bool` | `false` | no | | block\_public\_access\_acl | Related to block\_public\_access. PUT Bucket acl and PUT Object acl calls will fail if the specified ACL allows public access. PUT Object calls will fail if the request includes an object ACL. | `bool` | `true` | no | | block\_public\_access\_ignore\_acl | Related to block\_public\_access. Ignore public ACLs on this bucket and any objects that it contains. | `bool` | `true` | no | @@ -94,30 +95,26 @@ No Modules. | environment | Application environment for which this network is being created. must be one of ['Development', 'Integration', 'PreProduction', 'Production', 'QA', 'Staging', 'Test'] | `string` | `"Development"` | no | | expected\_bucket\_owner | The account ID of the expected bucket owner | `string` | `null` | no | | force\_destroy\_bucket | A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable. | `bool` | `false` | no | +| intelligent\_tiering | Map containing intelligent tiering configuration. | `any` | `{}` | no | | kms\_key\_id | The AWS KMS master key ID used for the SSE-KMS encryption. This can only be used when you set the value of sse\_algorithm as aws:kms. | `string` | `""` | no | | lifecycle\_enabled | Enable object lifecycle management. i.e. true \| false | `bool` | `false` | no | -| lifecycle\_rule\_prefix | Object keyname prefix identifying one or more objects to which the rule applies. Set as an empty string to target the whole bucket. | `string` | `""` | no | +| lifecycle\_rule | List of maps containing configuration of object lifecycle management. | `any` | `[]` | no | | logging\_bucket\_name | Name of the existing bucket where the logs will be stored. | `string` | `""` | no | | logging\_bucket\_prefix | Prefix for all log object keys. i.e. logs/ | `string` | `""` | no | +| metric\_configuration | Map containing bucket metric configuration. | `any` | `[]` | no | | mfa\_delete | Specifies whether MFA delete is enabled in the bucket versioning configuration | `bool` | `false` | no | | name | The name of the S3 bucket for the access logs. The bucket name can contain only lowercase letters, numbers, periods (.), and dashes (-). Must be globally unique. If changed, forces a new resource. | `string` | n/a | yes | -| noncurrent\_version\_expiration\_days | Indicates after how many days we are deleting previous version of objects. Set to 0 to disable or at least 365 days longer than noncurrent\_version\_transition\_glacier\_days. i.e. 0 to disable, 1-999 otherwise | `number` | `0` | no | -| noncurrent\_version\_transition\_glacier\_days | Indicates after how many days we are moving previous versions to Glacier. Should be 0 to disable or at least 30 days longer than noncurrent\_version\_transition\_ia\_days. i.e. 0 to disable, 1-999 otherwise | `number` | `0` | no | -| noncurrent\_version\_transition\_ia\_days | Indicates after how many days we are moving previous version objects to Standard-IA storage. Set to 0 to disable. | `number` | `0` | no | | object\_expiration\_days | Indicates after how many days we are deleting current version of objects. Set to 0 to disable or at least 365 days longer than TransitionInDaysGlacier. i.e. 0 to disable, otherwise 1-999 | `number` | `0` | no | | object\_lock\_enabled | Indicates whether this bucket has an Object Lock configuration enabled. Disabled by default. You can only enable S3 Object Lock for new buckets. If you need to turn on S3 Object Lock for an existing bucket, please contact AWS Support. | `bool` | `false` | no | | object\_lock\_mode | The default Object Lock retention mode you want to apply to new objects placed in this bucket. Valid values are GOVERNANCE and COMPLIANCE. Default is GOVERNANCE (allows administrative override). | `string` | `"GOVERNANCE"` | no | | object\_lock\_retention\_days | The retention of the object lock in days. Either days or years must be specified, but not both. | `number` | `null` | no | | object\_lock\_retention\_years | The retention of the object lock in years. Either days or years must be specified, but not both. | `number` | `null` | no | -| rax\_mpu\_cleanup\_enabled | Enable Rackspace default values for cleanup of Multipart Uploads. | `bool` | `true` | no | +| object\_lock\_token | A token to allow Object Lock to be enabled for an existing bucket. You must contact AWS support for the bucket's 'Object Lock token'. The token is generated in the back-end when versioning is enabled on a bucket. | `string` | `null` | no | | sse\_algorithm | The server-side encryption algorithm to use. Valid values are AES256, aws:kms, and none | `string` | `"AES256"` | no | | tags | A map of tags to be applied to the Bucket. i.e {Environment='Development'} | `map(string)` | `{}` | no | -| transition\_to\_glacier\_days | Indicates after how many days we are moving current versions to Glacier. Should be 0 to disable or at least 30 days longer than transition\_to\_ia\_days. i.e. 0 to disable, otherwise 1-999 | `number` | `0` | no | -| transition\_to\_ia\_days | Indicates after how many days we are moving current objects to Standard-IA storage. i.e. 0 to disable, otherwise 1-999 | `number` | `0` | no | | versioning | Enable bucket versioning. | `bool` | `false` | no | | website | Use bucket as a static website. i.e. true \| false | `bool` | `false` | no | -| website\_error | Location of Error HTML file. i.e. error.html | `string` | `"error.html"` | no | -| website\_index | Location of Index HTML file. i.e index.html | `string` | `"index.html"` | no | +| website\_config | Map containing static web-site hosting or redirect configuration. | `any` | `{}` | no | ## Outputs diff --git a/examples/s3.tf b/examples/s3.tf index 97302fe..7ed2643 100644 --- a/examples/s3.tf +++ b/examples/s3.tf @@ -1,10 +1,17 @@ terraform { - required_version = ">= 0.12" + required_version = ">= 0.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + + } + } } provider "aws" { - version = "~> 2.7" - region = "us-west-2" + region = "us-west-2" } resource "random_string" "s3_rstring" { @@ -13,27 +20,22 @@ resource "random_string" "s3_rstring" { upper = false } -module "s3" { - source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.3" - - bucket_logging = false - bucket_acl = "private" - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - object_lock_enabled = true - object_lock_mode = "GOVERNANCE" - object_lock_retention_days = 1 - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" +module "s3_basic" { + source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" + + bucket_logging = false + bucket_acl = "private" + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + lifecycle_enabled = true + lifecycle_rule = [ + { + id = "Default MPU Cleanup Rule." + enabled = true + abort_incomplete_multipart_upload_days = 7 + } + ] tags = { RightSaid = "Fred" @@ -42,22 +44,56 @@ module "s3" { } -module "s3_no_website" { - source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.3" +module "s3_website_with_cors" { + # Websites and CORS have undergone a significant refactor since v0.12.7 due to features that added to their complexity. + # Follow this example if you are using v0.12.10+ + source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" - bucket_logging = false - bucket_acl = "private" - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket-no-web" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = false + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + website = true + website_config = { + index_document = "index.html" + error_document = "error.html" + routing_rules = [{ + condition = { + key_prefix_equals = "docs/" + }, + redirect = { + replace_key_prefix_with = "documents/" + } + }, { + condition = { + http_error_code_returned_equals = 404 + key_prefix_equals = "archive/" + }, + redirect = { + host_name = "example.com" + http_redirect_code = 301 + protocol = "https" + replace_key_with = "not_found.html" + } + }] + } + cors = true + cors_rule = [ + { + allowed_methods = ["PUT", "POST"] + allowed_origins = ["https://modules.tf", "https://terraform-aws-modules.modules.tf"] + allowed_headers = ["*"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + }, { + allowed_methods = ["PUT"] + allowed_origins = ["https://example.com"] + allowed_headers = ["*"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } + ] tags = { RightSaid = "Fred" @@ -66,31 +102,158 @@ module "s3_no_website" { } module "s3_object_lock" { - source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.3" - - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - object_lock_enabled = true - object_lock_mode = "GOVERNANCE" - object_lock_retention_days = 1 - rax_mpu_cleanup_enabled = false - sse_algorithm = "none" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" + source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" + + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + object_lock_enabled = true + object_lock_mode = "GOVERNANCE" + object_lock_retention_days = 1 + versioning = true tags = { RightSaid = "Fred" LeftSaid = "George" } } + +module "s3_with_lifecycle" { + # Lifecycle has undergone a significant refactor since v0.12.7 due to features that added to their complexity. + # Follow this example if you are using v0.12.10+ + source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" + + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + lifecycle_enabled = true + lifecycle_rule = [ + { + id = "log" + enabled = true + + filter = { + tags = { + some = "value" + another = "value2" + } + } + + transition = [ + { + days = 30 + storage_class = "ONEZONE_IA" + }, { + days = 60 + storage_class = "GLACIER" + } + ] + }, + { + id = "Default MPU Cleanup Rule." + enabled = true + abort_incomplete_multipart_upload_days = 7 + + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + { + days = 60 + storage_class = "ONEZONE_IA" + }, + { + days = 90 + storage_class = "GLACIER" + }, + ] + + noncurrent_version_expiration = { + days = 300 + } + }, + { + id = "log2" + enabled = true + + filter = { + prefix = "log1/" + object_size_greater_than = 200000 + object_size_less_than = 500000 + tags = { + some = "value" + another = "value2" + } + } + + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + ] + + noncurrent_version_expiration = { + days = 300 + } + }, + ] + + intelligent_tiering = { + general = { + status = "Enabled" + filter = { + prefix = "/" + tags = { + Environment = "dev" + } + } + tiering = { + ARCHIVE_ACCESS = { + days = 180 + } + } + }, + documents = { + status = false + filter = { + prefix = "documents/" + } + tiering = { + ARCHIVE_ACCESS = { + days = 125 + } + DEEP_ARCHIVE_ACCESS = { + days = 200 + } + } + } + } + + metric_configuration = [ + { + name = "documents" + filter = { + prefix = "documents/" + tags = { + priority = "high" + } + } + }, + { + name = "other" + filter = { + tags = { + production = "true" + } + } + }, + { + name = "all" + } + ] +} diff --git a/main.tf b/main.tf index acf0620..55ed2b8 100644 --- a/main.tf +++ b/main.tf @@ -7,24 +7,22 @@ * ## Basic Usage * * ```HCL - * module "s3" { - * source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.3" + * module "s3_basic" { + * source = "git@github.com:rackspace-infrastructure-automation/aws-terraform-s3//?ref=v0.12.11" * - * bucket_acl = "private" - * bucket_logging = false - * environment = "Development" - * lifecycle_enabled = true - * name = "${random_string.s3_rstring.result}-example-s3-bucket" - * noncurrent_version_expiration_days = "425" - * noncurrent_version_transition_glacier_days = "60" - * noncurrent_version_transition_ia_days = "30" - * object_expiration_days = "425" - * transition_to_glacier_days = "60" - * transition_to_ia_days = "30" - * versioning = true - * website = true - * website_error = "error.html" - * website_index = "index.html" + * bucket_logging = false + * bucket_acl = "private" + * environment = "Development" + * name = "${random_string.s3_rstring.result}-example-s3-bucket" + * versioning = true + * lifecycle_enabled = true + * lifecycle_rule = [ + * { + * id = "Default MPU Cleanup Rule." + * enabled = true + * abort_incomplete_multipart_upload_days = 7 + * } + * ] * * tags = { * RightSaid = "Fred" @@ -50,6 +48,8 @@ * */ +data "aws_canonical_user_id" "this" {} + terraform { required_version = ">= 0.13" @@ -72,201 +72,19 @@ locals { default_tags = { ServiceProvider = "Rackspace" Environment = var.environment - SkipBucket = var.rax_mpu_cleanup_enabled ? null : "True" - } - - cors_rules = try(jsondecode(var.cors_rule), var.cors_rule) - - ############################################################## - # Lifecycle Rules local variables - ############################################################## - - lifecycle_rules = { - enabled = [ - { - enabled = var.lifecycle_enabled - expiration = local.object_expiration[var.object_expiration_days > 0 ? "enabled" : "disabled"] - noncurrent_version_expiration = local.noncurrent_version_expiration[var.noncurrent_version_expiration_days > 0 ? "enabled" : "disabled"] - prefix = var.lifecycle_rule_prefix - - noncurrent_version_transition = concat( - local.noncurrent_version_transition[var.noncurrent_version_transition_ia_days > 0 ? "ia_enabled" : "disabled"], - local.noncurrent_version_transition[var.noncurrent_version_transition_glacier_days > 0 ? "glacier_enabled" : "disabled"], - ) - - transition = concat( - local.transition[var.transition_to_ia_days > 0 ? "ia_enabled" : "disabled"], - local.transition[var.transition_to_glacier_days > 0 ? "glacier_enabled" : "disabled"], - ) - }, - ] - mpu_cleanup_enabled = [ - { - abort_incomplete_multipart_upload_days = var.abort_incomplete_multipart_upload_days - enabled = true - id = "rax-cleanup-incomplete-mpu-objects" - expiration = [{}] - }, - ] - disabled = [] - } - - object_expiration = { - enabled = [{ days = var.object_expiration_days }] - disabled = [] - } - - noncurrent_version_expiration = { - enabled = [{ days = var.noncurrent_version_expiration_days }] - disabled = [] } - noncurrent_version_transition = { - ia_enabled = [ - { - days = var.noncurrent_version_transition_ia_days - storage_class = "STANDARD_IA" - }, - ] - glacier_enabled = [ - { - days = var.noncurrent_version_transition_glacier_days - storage_class = "GLACIER" - }, - ] - disabled = [] - } - - transition = { - ia_enabled = [ - { - days = var.transition_to_ia_days - storage_class = "STANDARD_IA" - }, - ] - glacier_enabled = [ - { - days = var.transition_to_glacier_days - storage_class = "GLACIER" - }, - ] - disabled = [] - } - - ############################################################## - # Bucket object lock local variables - ############################################################## - - object_lock_rule = { - Enabled = [ - { - rule = [ - { - mode = var.object_lock_mode - days = var.object_lock_retention_days - years = var.object_lock_retention_years - }, - ] - }, - ] - Disabled = [] - } - - ############################################################## - # Bucket website local variables - ############################################################## + cors_rules = try(jsondecode(var.cors_rule), var.cors_rule) + lifecycle_rules = try(jsondecode(var.lifecycle_rule), var.lifecycle_rule) + intelligent_tiering = try(jsondecode(var.intelligent_tiering), var.intelligent_tiering) + metric_configuration = try(jsondecode(var.metric_configuration), var.metric_configuration) - bucket_website_config = { - enabled = [ - { - index_document = var.website_index - error_document = var.website_error - }, - ] - disabled = [] - } } resource "aws_s3_bucket" "s3_bucket" { bucket = var.name force_destroy = var.force_destroy_bucket tags = merge(var.tags, local.default_tags) - - dynamic "lifecycle_rule" { - for_each = concat( - local.lifecycle_rules[(var.lifecycle_enabled ? "enabled" : "disabled")], - local.lifecycle_rules[(var.rax_mpu_cleanup_enabled ? "mpu_cleanup_enabled" : "disabled")] - ) - - content { - abort_incomplete_multipart_upload_days = lookup(lifecycle_rule.value, "abort_incomplete_multipart_upload_days", null) - enabled = lifecycle_rule.value.enabled - id = lookup(lifecycle_rule.value, "id", null) - prefix = lookup(lifecycle_rule.value, "prefix", null) - tags = lookup(lifecycle_rule.value, "tags", null) - - dynamic "expiration" { - for_each = lookup(lifecycle_rule.value, "expiration", []) - content { - date = lookup(expiration.value, "date", null) - days = lookup(expiration.value, "days", null) - expired_object_delete_marker = lookup(expiration.value, "expired_object_delete_marker", null) - } - } - - dynamic "noncurrent_version_expiration" { - for_each = lookup(lifecycle_rule.value, "noncurrent_version_expiration", []) - content { - days = noncurrent_version_expiration.value.days - } - } - - dynamic "noncurrent_version_transition" { - for_each = lookup(lifecycle_rule.value, "noncurrent_version_transition", []) - content { - days = noncurrent_version_transition.value.days - storage_class = noncurrent_version_transition.value.storage_class - } - } - - dynamic "transition" { - for_each = lookup(lifecycle_rule.value, "transition", []) - content { - date = lookup(transition.value, "date", null) - days = lookup(transition.value, "days", null) - storage_class = transition.value.storage_class - } - } - } - } - - dynamic "object_lock_configuration" { - for_each = local.object_lock_rule[var.object_lock_enabled ? "Enabled" : "Disabled"] - content { - object_lock_enabled = "Enabled" // The only valid value when this configuration is present - dynamic "rule" { - for_each = lookup(object_lock_configuration.value, "rule", []) - content { - default_retention { - mode = lookup(rule.value, "mode", "GOVERNANCE") - days = lookup(rule.value, "days", null) - years = lookup(rule.value, "years", null) - } - } - } - } - } - - - dynamic "website" { - for_each = local.bucket_website_config[var.website ? "enabled" : "disabled"] - content { - error_document = lookup(website.value, "error_document", null) - index_document = lookup(website.value, "index_document", null) - redirect_all_requests_to = lookup(website.value, "redirect_all_requests_to", null) - routing_rules = lookup(website.value, "routing_rules", null) - } - } } ############################################################## @@ -296,7 +114,7 @@ resource "aws_s3_bucket_acl" "s3_acl" { ############################################################## # S3 Versioning Configuration ############################################################## -resource "aws_s3_bucket_versioning" "s3_versioning" { +resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.s3_bucket.id versioning_configuration { status = var.versioning ? "Enabled" : "Disabled" @@ -353,3 +171,198 @@ resource "aws_s3_bucket_cors_configuration" "this" { } } } + +############################################################## +# S3 Object Lock Configuration +############################################################## +resource "aws_s3_bucket_object_lock_configuration" "this" { + count = var.object_lock_enabled ? 1 : 0 + + bucket = aws_s3_bucket.s3_bucket.id + expected_bucket_owner = var.expected_bucket_owner + token = try(var.object_lock_token, null) + + rule { + default_retention { + mode = var.object_lock_mode + days = try(var.object_lock_retention_days, null) + years = try(var.object_lock_retention_years, null) + } + } +} + +############################################################## +# S3 Website Configuration +############################################################## +resource "aws_s3_bucket_website_configuration" "this" { + count = var.website ? 1 : 0 + + bucket = aws_s3_bucket.s3_bucket.id + expected_bucket_owner = var.expected_bucket_owner + + dynamic "index_document" { + for_each = try([var.website_config["index_document"]], []) + + content { + suffix = index_document.value + } + } + + dynamic "error_document" { + for_each = try([var.website_config["error_document"]], []) + + content { + key = error_document.value + } + } + + dynamic "redirect_all_requests_to" { + for_each = try([var.website_config["redirect_all_requests_to"]], []) + + content { + host_name = redirect_all_requests_to.value.host_name + protocol = try(redirect_all_requests_to.value.protocol, null) + } + } + + dynamic "routing_rule" { + for_each = try(flatten([var.website_config["routing_rules"]]), []) + + content { + dynamic "condition" { + for_each = [try([routing_rule.value.condition], [])] + + content { + http_error_code_returned_equals = try(routing_rule.value.condition["http_error_code_returned_equals"], null) + key_prefix_equals = try(routing_rule.value.condition["key_prefix_equals"], null) + } + } + + redirect { + host_name = try(routing_rule.value.redirect["host_name"], null) + http_redirect_code = try(routing_rule.value.redirect["http_redirect_code"], null) + protocol = try(routing_rule.value.redirect["protocol"], null) + replace_key_prefix_with = try(routing_rule.value.redirect["replace_key_prefix_with"], null) + replace_key_with = try(routing_rule.value.redirect["replace_key_with"], null) + } + } + } +} + +############################################################## +# S3 Lifecycle Configuration +############################################################## +resource "aws_s3_bucket_lifecycle_configuration" "this" { + count = var.lifecycle_enabled ? 1 : 0 + + bucket = aws_s3_bucket.s3_bucket.id + expected_bucket_owner = var.expected_bucket_owner + + dynamic "rule" { + for_each = local.lifecycle_rules + + content { + id = try(rule.value.id, null) + status = try(rule.value.enabled ? "Enabled" : "Disabled", tobool(rule.value.status) ? "Enabled" : "Disabled", title(lower(rule.value.status))) + + # Only one rule allowed - abort_incomplete_multipart_upload + dynamic "abort_incomplete_multipart_upload" { + for_each = try([rule.value.abort_incomplete_multipart_upload_days], []) + + content { + days_after_initiation = try(rule.value.abort_incomplete_multipart_upload_days, null) + } + } + + + # Only one rule allowed - expiration + dynamic "expiration" { + for_each = try(flatten([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) + } + } + + # Multiple rules allowed - transition + dynamic "transition" { + for_each = try(flatten([rule.value.transition]), []) + + content { + date = try(transition.value.date, null) + days = try(transition.value.days, null) + storage_class = transition.value.storage_class + } + } + + # Only one rule allowed - noncurrent_version_expiration + dynamic "noncurrent_version_expiration" { + for_each = try(flatten([rule.value.noncurrent_version_expiration]), []) + + content { + newer_noncurrent_versions = try(noncurrent_version_expiration.value.newer_noncurrent_versions, null) + noncurrent_days = try(noncurrent_version_expiration.value.days, noncurrent_version_expiration.value.noncurrent_days, null) + } + } + + # Multiple rules allowed - noncurrent_version_transition + dynamic "noncurrent_version_transition" { + for_each = try(flatten([rule.value.noncurrent_version_transition]), []) + + content { + newer_noncurrent_versions = try(noncurrent_version_transition.value.newer_noncurrent_versions, null) + noncurrent_days = try(noncurrent_version_transition.value.days, noncurrent_version_transition.value.noncurrent_days, null) + storage_class = noncurrent_version_transition.value.storage_class + } + } + + # Only one rule allowed - filter - without any key arguments or tags + dynamic "filter" { + for_each = length(try(flatten([rule.value.filter]), [])) == 0 ? [true] : [] + + content { + + } + } + + # Only one rule allowed - filter - with one key argument or a single tag + dynamic "filter" { + for_each = [for v in try(flatten([rule.value.filter]), []) : v if max(length(keys(v)), length(try(rule.value.filter.tags, rule.value.filter.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(filter.value.tags, filter.value.tag, []) + + content { + key = tag.key + value = tag.value + } + } + } + } + + # Only one rule allowed - filter - with more than one key arguments or multiple tags + dynamic "filter" { + for_each = [for v in try(flatten([rule.value.filter]), []) : v if max(length(keys(v)), length(try(rule.value.filter.tags, rule.value.filter.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) + } + } + } + } + } + + # Requires versioning enabled to build + depends_on = [aws_s3_bucket_versioning.this] +} diff --git a/tests/test1/main.tf b/tests/test1/main.tf index e90857f..9115bda 100644 --- a/tests/test1/main.tf +++ b/tests/test1/main.tf @@ -1,14 +1,25 @@ +### +# Basic bucket test +### + terraform { - required_version = ">= 0.12" + required_version = ">= 0.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + + } + } } provider "aws" { - version = "~> 3.0" - region = "us-west-2" + region = "us-west-2" } resource "random_string" "s3_rstring" { - length = 18 + length = 16 special = false upper = false } @@ -16,21 +27,19 @@ resource "random_string" "s3_rstring" { module "s3" { source = "../../module" - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + lifecycle_enabled = true + lifecycle_rule = [ + { + id = "Default MPU Cleanup Rule." + enabled = true + abort_incomplete_multipart_upload_days = 7 + } + ] tags = { RightSaid = "Fred" diff --git a/tests/test2/main.tf b/tests/test2/main.tf index b204f8b..794ebc0 100644 --- a/tests/test2/main.tf +++ b/tests/test2/main.tf @@ -1,18 +1,25 @@ ### -# This test adds the 'CORS Rules' configuration variables +# Website with CORS rules test ### terraform { - required_version = ">= 0.12" + required_version = ">= 0.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + + } + } } provider "aws" { - version = "~> 3.0" - region = "us-west-2" + region = "us-west-2" } resource "random_string" "s3_rstring" { - length = 18 + length = 16 special = false upper = false } @@ -20,22 +27,36 @@ resource "random_string" "s3_rstring" { module "s3" { source = "../../module" - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" - cors = true + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + website = true + website_config = { + index_document = "index.html" + error_document = "error.html" + routing_rules = [{ + condition = { + key_prefix_equals = "docs/" + }, + redirect = { + replace_key_prefix_with = "documents/" + } + }, { + condition = { + http_error_code_returned_equals = 404 + key_prefix_equals = "archive/" + }, + redirect = { + host_name = "example.com" + http_redirect_code = 301 + protocol = "https" + replace_key_with = "not_found.html" + } + }] + } + cors = true cors_rule = [ { allowed_methods = ["PUT", "POST"] @@ -52,9 +73,6 @@ module "s3" { } ] - # Not defining these to ensure it can properly handle undefined variable lists or strings - # expose_headers = ["Accept-Ranges", "Content-Range", "Content-Encoding", "Content-Length"] - tags = { RightSaid = "Fred" LeftSaid = "George" diff --git a/tests/test3/main.tf b/tests/test3/main.tf index 63fe72b..a069f5a 100644 --- a/tests/test3/main.tf +++ b/tests/test3/main.tf @@ -1,76 +1,162 @@ ### -# This test adds the sse_algorithm option 'none' and disabled MPU cleanup +# Complex Lifecycle Tests ### terraform { - required_version = ">= 0.12" + required_version = ">= 0.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + + } + } } provider "aws" { - version = "~> 3.0" - region = "us-west-2" + region = "us-west-2" } resource "random_string" "s3_rstring" { - length = 18 + length = 16 special = false upper = false } -module "s3" { +module "s3_lifecycle" { source = "../../module" - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - rax_mpu_cleanup_enabled = false - sse_algorithm = "none" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" - - tags = { - RightSaid = "Fred" - LeftSaid = "George" - } -} + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + lifecycle_enabled = true + lifecycle_rule = [ + { + id = "log" + enabled = true -module "s3_logging_test" { - source = "../../module" + filter = { + tags = { + some = "value" + another = "value2" + } + } + + transition = [ + { + days = 30 + storage_class = "ONEZONE_IA" + }, { + days = 60 + storage_class = "GLACIER" + } + ] + }, + { + id = "Default MPU Cleanup Rule." + enabled = true + abort_incomplete_multipart_upload_days = 7 + + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + { + days = 60 + storage_class = "ONEZONE_IA" + }, + { + days = 90 + storage_class = "GLACIER" + }, + ] + + noncurrent_version_expiration = { + days = 300 + } + }, + { + id = "log2" + enabled = true + + filter = { + prefix = "log1/" + object_size_greater_than = 200000 + object_size_less_than = 500000 + tags = { + some = "value" + another = "value2" + } + } + + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + }, + ] + + noncurrent_version_expiration = { + days = 300 + } + }, + ] - bucket_acl = "private" - bucket_logging = true - logging_bucket_name = module.s3.bucket_id - logging_bucket_prefix = "logs/" - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-log-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - rax_mpu_cleanup_enabled = true - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - kms_key_id = "aws/s3" - sse_algorithm = "aws:kms" - bucket_key_enabled = true - - - tags = { - RightSaid = "Fred" - LeftSaid = "George" + intelligent_tiering = { + general = { + status = "Enabled" + filter = { + prefix = "/" + tags = { + Environment = "dev" + } + } + tiering = { + ARCHIVE_ACCESS = { + days = 180 + } + } + }, + documents = { + status = false + filter = { + prefix = "documents/" + } + tiering = { + ARCHIVE_ACCESS = { + days = 125 + } + DEEP_ARCHIVE_ACCESS = { + days = 200 + } + } + } } - depends_on = [module.s3] + metric_configuration = [ + { + name = "documents" + filter = { + prefix = "documents/" + tags = { + priority = "high" + } + } + }, + { + name = "other" + filter = { + tags = { + production = "true" + } + } + }, + { + name = "all" + } + ] } diff --git a/tests/test4/main.tf b/tests/test4/main.tf index d759484..b936cbe 100644 --- a/tests/test4/main.tf +++ b/tests/test4/main.tf @@ -1,18 +1,25 @@ ### -# This test adds the sse_algorithm option 'none' and disabled MPU cleanup +# SSE & bucket key test ### terraform { - required_version = ">= 0.12" + required_version = ">= 0.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + + } + } } provider "aws" { - version = "~> 3.0" - region = "us-west-2" + region = "us-west-2" } resource "random_string" "s3_rstring" { - length = 18 + length = 16 special = false upper = false } @@ -20,26 +27,14 @@ resource "random_string" "s3_rstring" { module "s3" { source = "../../module" - bucket_acl = "private" - bucket_logging = false - environment = "Development" - lifecycle_enabled = true - name = "${random_string.s3_rstring.result}-example-s3-bucket" - noncurrent_version_expiration_days = "425" - noncurrent_version_transition_glacier_days = "60" - noncurrent_version_transition_ia_days = "30" - object_expiration_days = "425" - object_lock_enabled = true - object_lock_mode = "GOVERNANCE" - object_lock_retention_days = 1 - rax_mpu_cleanup_enabled = false - sse_algorithm = "none" - transition_to_glacier_days = "60" - transition_to_ia_days = "30" - versioning = true - website = true - website_error = "error.html" - website_index = "index.html" + bucket_acl = "private" + bucket_logging = false + environment = "Development" + name = "${random_string.s3_rstring.result}-example-s3-bucket" + versioning = true + kms_key_id = "aws/s3" + sse_algorithm = "aws:kms" + bucket_key_enabled = true tags = { RightSaid = "Fred" diff --git a/variables.tf b/variables.tf index 5b02328..eadbc99 100644 --- a/variables.tf +++ b/variables.tf @@ -71,16 +71,22 @@ variable "lifecycle_enabled" { default = false } -variable "lifecycle_rule_prefix" { - description = "Object keyname prefix identifying one or more objects to which the rule applies. Set as an empty string to target the whole bucket." - type = string - default = "" +variable "lifecycle_rule" { + description = "List of maps containing configuration of object lifecycle management." + type = any + default = [] } -variable "abort_incomplete_multipart_upload_days" { - description = "Abort Incomplete Multipart Upload Days i.e. 7 | 0" - type = number - default = 7 +variable "intelligent_tiering" { + description = "Map containing intelligent tiering configuration." + type = any + default = {} +} + +variable "metric_configuration" { + description = "Map containing bucket metric configuration." + type = any + default = [] } variable "logging_bucket_name" { @@ -100,24 +106,6 @@ variable "name" { type = string } -variable "noncurrent_version_expiration_days" { - description = "Indicates after how many days we are deleting previous version of objects. Set to 0 to disable or at least 365 days longer than noncurrent_version_transition_glacier_days. i.e. 0 to disable, 1-999 otherwise" - type = number - default = 0 -} - -variable "noncurrent_version_transition_glacier_days" { - description = "Indicates after how many days we are moving previous versions to Glacier. Should be 0 to disable or at least 30 days longer than noncurrent_version_transition_ia_days. i.e. 0 to disable, 1-999 otherwise" - type = number - default = 0 -} - -variable "noncurrent_version_transition_ia_days" { - description = "Indicates after how many days we are moving previous version objects to Standard-IA storage. Set to 0 to disable." - type = number - default = 0 -} - variable "object_expiration_days" { description = "Indicates after how many days we are deleting current version of objects. Set to 0 to disable or at least 365 days longer than TransitionInDaysGlacier. i.e. 0 to disable, otherwise 1-999" type = number @@ -129,28 +117,31 @@ variable "object_lock_enabled" { type = bool default = false } + +variable "object_lock_token" { + description = "A token to allow Object Lock to be enabled for an existing bucket. You must contact AWS support for the bucket's 'Object Lock token'. The token is generated in the back-end when versioning is enabled on a bucket." + type = string + default = null +} + variable "object_lock_mode" { description = "The default Object Lock retention mode you want to apply to new objects placed in this bucket. Valid values are GOVERNANCE and COMPLIANCE. Default is GOVERNANCE (allows administrative override)." type = string default = "GOVERNANCE" } + variable "object_lock_retention_days" { description = "The retention of the object lock in days. Either days or years must be specified, but not both." type = number default = null } + variable "object_lock_retention_years" { description = "The retention of the object lock in years. Either days or years must be specified, but not both." type = number default = null } -variable "rax_mpu_cleanup_enabled" { - description = "Enable Rackspace default values for cleanup of Multipart Uploads." - type = bool - default = true -} - variable "sse_algorithm" { description = "The server-side encryption algorithm to use. Valid values are AES256, aws:kms, and none" type = string @@ -163,18 +154,6 @@ variable "tags" { default = {} } -variable "transition_to_glacier_days" { - description = "Indicates after how many days we are moving current versions to Glacier. Should be 0 to disable or at least 30 days longer than transition_to_ia_days. i.e. 0 to disable, otherwise 1-999" - type = number - default = 0 -} - -variable "transition_to_ia_days" { - description = "Indicates after how many days we are moving current objects to Standard-IA storage. i.e. 0 to disable, otherwise 1-999" - type = number - default = 0 -} - variable "versioning" { description = "Enable bucket versioning." type = bool @@ -193,16 +172,10 @@ variable "website" { default = false } -variable "website_error" { - description = "Location of Error HTML file. i.e. error.html" - type = string - default = "error.html" -} - -variable "website_index" { - description = "Location of Index HTML file. i.e index.html" - type = string - default = "index.html" +variable "website_config" { + description = "Map containing static web-site hosting or redirect configuration." + type = any # map(string) + default = {} } variable "cors" {