From 618a71b9514fb1705379286bfbe294d1867fa1fa Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 25 Feb 2022 04:08:41 -0800 Subject: [PATCH] Full support for lifecycle configuration (#135) --- .github/workflows/auto-context.yml | 2 +- .github/workflows/auto-format.yml | 2 +- .github/workflows/chatops.yml | 4 +- README.md | 75 +++++-- README.yaml | 55 ++++- docs/terraform.md | 20 +- examples/complete/lifecycle.us-east-2.tfvars | 41 +++- examples/complete/main.tf | 1 + examples/complete/privileged-principals.tf | 15 +- examples/complete/variables.tf | 20 ++ lifecycle.tf | 223 +++++++++++++++++++ main.tf | 140 ++---------- test/src/go.sum | 2 - variables-deprecated.tf | 65 ++---- variables.tf | 160 +++++++------ 15 files changed, 540 insertions(+), 285 deletions(-) create mode 100644 lifecycle.tf diff --git a/.github/workflows/auto-context.yml b/.github/workflows/auto-context.yml index ab979e0e..665833af 100644 --- a/.github/workflows/auto-context.yml +++ b/.github/workflows/auto-context.yml @@ -35,7 +35,7 @@ jobs: - name: Create Pull Request if: steps.update.outputs.create_pull_request == 'true' - uses: cloudposse/actions/github/create-pull-request@0.22.0 + uses: cloudposse/actions/github/create-pull-request@0.30.0 with: token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} committer: 'cloudpossebot <11232728+cloudpossebot@users.noreply.github.com>' diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml index 375d0fd4..c600d602 100644 --- a/.github/workflows/auto-format.yml +++ b/.github/workflows/auto-format.yml @@ -62,7 +62,7 @@ jobs: fi - name: Auto Test - uses: cloudposse/actions/github/repository-dispatch@0.22.0 + uses: cloudposse/actions/github/repository-dispatch@0.30.0 # match users by ID because logins (user names) are inconsistent, # for example in the REST API Renovate Bot is `renovate[bot]` but # in GraphQL it is just `renovate`, plus there is a non-bot diff --git a/.github/workflows/chatops.yml b/.github/workflows/chatops.yml index 4ddc0674..23f96d82 100644 --- a/.github/workflows/chatops.yml +++ b/.github/workflows/chatops.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: "Handle common commands" - uses: cloudposse/actions/github/slash-command-dispatch@0.22.0 + uses: cloudposse/actions/github/slash-command-dispatch@0.30.0 with: token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} reaction-token: ${{ secrets.GITHUB_TOKEN }} @@ -24,7 +24,7 @@ jobs: - name: "Checkout commit" uses: actions/checkout@v2 - name: "Run tests" - uses: cloudposse/actions/github/slash-command-dispatch@0.22.0 + uses: cloudposse/actions/github/slash-command-dispatch@0.30.0 with: token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} reaction-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 80bb3d2c..11623c01 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,10 @@ --> -This module creates an S3 bucket with support of versioning, replication, encryption, ACL, and bucket object policy. -If `user_enabled` variable is set to `true`, the module will provision a basic IAM user with permissions to access the bucket. +This module creates an S3 bucket with support for versioning, lifecycles, object locks, replication, encryption, ACL, +bucket object policies, and static website hosting. +If `user_enabled` variable is set to `true`, the module will provision a basic IAM user with permissions to access the bucket. This basic IAM system user is suitable for CI/CD systems (_e.g._ TravisCI, CircleCI) or systems which are *external* to AWS that cannot leverage [AWS IAM Instance Profiles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) and do not already have IAM credentials. Users or systems that have IAM credentials should either be granted access directly based on @@ -39,7 +40,7 @@ their IAM identity or be allowed to assume an IAM role with access. We do not recommend creating IAM users this way for any other purpose. This module blocks public access to the bucket by default. See `block_public_acls`, `block_public_policy`, -and `ignore_public_acls` to change the settings. See [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html) +`ignore_public_acls`, and `restrict_public_buckets` to change the settings. See [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html) for more details. --- @@ -123,7 +124,43 @@ module "s3_bucket" { } ``` -Using [grants](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html) to enable access to another account and for logging. +Configuring S3 [storage lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html): + +```hcl +locals { + lifecycle_configuration_rules = [{ + enabled = true # bool + id = "v2rule" + + abort_incomplete_multipart_upload_days = 1 # number + + filter_and = null + expiration = { + days = 120 # integer > 0 + } + noncurrent_version_expiration = { + newer_noncurrent_versions = 3 # integer > 0 + noncurrent_days = 60 # integer >= 0 + } + transition = [{ + days = 30 # integer >= 0 + storage_class = "STANDARD_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }, + { + days = 60 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + noncurrent_version_transition = [{ + newer_noncurrent_versions = 3 # integer >= 0 + noncurrent_days = 30 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + }] +} +``` + +Using [grants](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html) to enable access +to another account and for logging, and incorporating the above lifecycle configuration. ```hcl module "s3_bucket" { @@ -133,12 +170,14 @@ module "s3_bucket" { acl = "" enabled = true user_enabled = true - versioning_enabled = false + versioning_enabled = true allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"] name = "app" stage = "test" namespace = "eg" + lifecycle_configuration_rules = local.lifecycle_configuration_rules + grants = [ { id = "012abc345def678ghi901" # Canonical user or account id @@ -172,10 +211,12 @@ module "s3_bucket" { stage = "test" namespace = "eg" - privileged_principal_arns = { + privileged_principal_arns = [ + { "arn:aws:iam::123456789012:role/principal1" = ["prefix1/", "prefix2/"] + }, { "arn:aws:iam::123456789012:role/principal2" = [""] - } + }] privileged_principal_actions = [ "s3:PutObject", "s3:PutObjectAcl", @@ -263,8 +304,7 @@ Available targets: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [acl](#input\_acl) | The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. | `string` | `"private"` | no | -| [acl\_grants](#input\_acl\_grants) | A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permission = string
uri = string
}))
| `null` | no | +| [acl](#input\_acl) | The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply.
We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. | `string` | `"private"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [allow\_encrypted\_uploads\_only](#input\_allow\_encrypted\_uploads\_only) | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | `bool` | `false` | no | | [allow\_ssl\_requests\_only](#input\_allow\_ssl\_requests\_only) | Set to `true` to require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests | `bool` | `false` | no | @@ -280,8 +320,8 @@ Available targets: | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [force\_destroy](#input\_force\_destroy) | A boolean string 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 | -| [grants](#input\_grants) | DEPRECATED (replaced by `acl_grants`): A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permissions = list(string)
uri = string
}))
| `null` | no | +| [force\_destroy](#input\_force\_destroy) | When `true`, permits a non-empty S3 bucket to be deleted by first deleting all objects in the bucket.
THESE OBJECTS ARE NOT RECOVERABLE even if they were versioned and stored in Glacier. | `bool` | `false` | no | +| [grants](#input\_grants) | A list of policy grants for the bucket, taking a list of permissions.
Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permissions = list(string)
uri = string
}))
| `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ignore\_public\_acls](#input\_ignore\_public\_acls) | Set to `false` to disable the ignoring of public access lists on the bucket | `bool` | `true` | no | | [kms\_master\_key\_arn](#input\_kms\_master\_key\_arn) | The AWS KMS master key ARN used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms` | `string` | `""` | no | @@ -289,24 +329,25 @@ Available targets: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [lifecycle\_configuration\_rules](#input\_lifecycle\_configuration\_rules) | A list of lifecycle rules |
list(object({
id = string
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
|
[
{
"abort_incomplete_multipart_upload_days": 90,
"deeparchive_transition_days": 90,
"enable_current_object_expiration": true,
"enable_deeparchive_transition": false,
"enable_glacier_transition": true,
"enable_noncurrent_version_expiration": true,
"enable_standard_ia_transition": false,
"enabled": false,
"expiration_days": 90,
"glacier_transition_days": 60,
"id": "noop",
"noncurrent_version_deeparchive_transition_days": 60,
"noncurrent_version_expiration_days": 90,
"noncurrent_version_glacier_transition_days": 30,
"prefix": "",
"standard_transition_days": 30,
"tags": {}
}
]
| no | -| [lifecycle\_rules](#input\_lifecycle\_rules) | DEPRECATED: A list of lifecycle rules |
list(object({
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
| `null` | no | +| [lifecycle\_configuration\_rules](#input\_lifecycle\_configuration\_rules) | A list of lifecycle V2 rules |
list(object({
enabled = bool
id = string

abort_incomplete_multipart_upload_days = number

# `filter_and` is the `and` configuration block inside the `filter` configuration.
# This is the only place you should specify a prefix.
filter_and = any
expiration = any
transition = list(any)

noncurrent_version_expiration = any
noncurrent_version_transition = list(any)
}))
| `[]` | no | +| [lifecycle\_rule\_ids](#input\_lifecycle\_rule\_ids) | DEPRECATED (use `lifecycle_configuration_rules`): A list of IDs to assign to corresponding `lifecycle_rules` | `list(string)` | `[]` | no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | DEPRECATED (`use lifecycle_configuration_rules`): A list of lifecycle rules |
list(object({
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
| `null` | no | | [logging](#input\_logging) | Bucket access logging configuration. |
object({
bucket_name = string
prefix = string
})
| `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [object\_lock\_configuration](#input\_object\_lock\_configuration) | A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. |
object({
mode = string # Valid values are GOVERNANCE and COMPLIANCE.
days = number
years = number
})
| `null` | no | -| [policy](#input\_policy) | DEPRECATED: A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no | +| [policy](#input\_policy) | DEPRECATED (use `source_policy_documents`): A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no | | [privileged\_principal\_actions](#input\_privileged\_principal\_actions) | List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`) | `list(string)` | `[]` | no | -| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions.
Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'. | `map(list(string))` | `{}` | no | +| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | List of maps. Each map has one key, an IAM Principal ARN, whose associated value is
a list of S3 path prefixes to grant `privileged_principal_actions` permissions for that principal,
in addition to the bucket itself, which is automatically included. Prefixes should not begin with '/'. | `list(map(list(string)))` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [replication\_rules](#input\_replication\_rules) | DEPRECATED: Use s3\_replication\_rules instead. | `list(any)` | `null` | no | +| [replication\_rules](#input\_replication\_rules) | DEPRECATED (use `s3_replication_rules`): Specifies the replication rules for S3 bucket replication if enabled. You must also set s3\_replication\_enabled to true. | `list(any)` | `null` | no | | [restrict\_public\_buckets](#input\_restrict\_public\_buckets) | Set to `false` to disable the restricting of making the bucket public | `bool` | `true` | no | | [s3\_object\_ownership](#input\_s3\_object\_ownership) | Specifies the S3 object ownership control. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'. | `string` | `"ObjectWriter"` | no | | [s3\_replica\_bucket\_arn](#input\_s3\_replica\_bucket\_arn) | A single S3 bucket ARN to use for all replication rules.
Note: The destination bucket can be specified in the replication rule itself
(which allows for multiple destinations), in which case it will take precedence over this variable. | `string` | `""` | no | | [s3\_replication\_enabled](#input\_s3\_replication\_enabled) | Set this to true and specify `s3_replication_rules` to enable replication. `versioning_enabled` must also be `true`. | `bool` | `false` | no | | [s3\_replication\_rules](#input\_s3\_replication\_rules) | Specifies the replication rules for S3 bucket replication if enabled. You must also set s3\_replication\_enabled to true. | `list(any)` | `null` | no | | [s3\_replication\_source\_roles](#input\_s3\_replication\_source\_roles) | Cross-account IAM Role ARNs that will be allowed to perform S3 replication to this bucket (for replication within the same AWS account, it's not necessary to adjust the bucket policy). | `list(string)` | `[]` | no | -| [source\_policy\_documents](#input\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements defined in source\_policy\_documents or source\_json must have unique sids. Statements with the same sid from documents assigned to the override\_json and override\_policy\_documents arguments will override source statements. | `list(string)` | `[]` | no | +| [source\_policy\_documents](#input\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document.
Statements defined in source\_policy\_documents or source\_json must have unique SIDs.
Statement having SIDs that match policy SIDs generated by this module will override them. | `list(string)` | `[]` | no | | [sse\_algorithm](#input\_sse\_algorithm) | The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms` | `string` | `"AES256"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/README.yaml b/README.yaml index 2d29189f..66c9444c 100644 --- a/README.yaml +++ b/README.yaml @@ -66,9 +66,10 @@ related: # Short description of this project description: |- - This module creates an S3 bucket with support of versioning, replication, encryption, ACL, and bucket object policy. + This module creates an S3 bucket with support for versioning, lifecycles, object locks, replication, encryption, ACL, + bucket object policies, and static website hosting. + If `user_enabled` variable is set to `true`, the module will provision a basic IAM user with permissions to access the bucket. - This basic IAM system user is suitable for CI/CD systems (_e.g._ TravisCI, CircleCI) or systems which are *external* to AWS that cannot leverage [AWS IAM Instance Profiles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) and do not already have IAM credentials. Users or systems that have IAM credentials should either be granted access directly based on @@ -77,7 +78,7 @@ description: |- We do not recommend creating IAM users this way for any other purpose. This module blocks public access to the bucket by default. See `block_public_acls`, `block_public_policy`, - and `ignore_public_acls` to change the settings. See [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html) + `ignore_public_acls`, and `restrict_public_buckets` to change the settings. See [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html) for more details. # How to use this project @@ -100,7 +101,43 @@ usage: |- } ``` - Using [grants](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html) to enable access to another account and for logging. + Configuring S3 [storage lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html): + + ```hcl + locals { + lifecycle_configuration_rules = [{ + enabled = true # bool + id = "v2rule" + + abort_incomplete_multipart_upload_days = 1 # number + + filter_and = null + expiration = { + days = 120 # integer > 0 + } + noncurrent_version_expiration = { + newer_noncurrent_versions = 3 # integer > 0 + noncurrent_days = 60 # integer >= 0 + } + transition = [{ + days = 30 # integer >= 0 + storage_class = "STANDARD_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }, + { + days = 60 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + noncurrent_version_transition = [{ + newer_noncurrent_versions = 3 # integer >= 0 + noncurrent_days = 30 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + }] + } + ``` + + Using [grants](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html) to enable access + to another account and for logging, and incorporating the above lifecycle configuration. ```hcl module "s3_bucket" { @@ -110,11 +147,13 @@ usage: |- acl = "" enabled = true user_enabled = true - versioning_enabled = false + versioning_enabled = true allowed_bucket_actions = ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"] name = "app" stage = "test" namespace = "eg" + + lifecycle_configuration_rules = local.lifecycle_configuration_rules grants = [ { @@ -149,10 +188,12 @@ usage: |- stage = "test" namespace = "eg" - privileged_principal_arns = { + privileged_principal_arns = [ + { "arn:aws:iam::123456789012:role/principal1" = ["prefix1/", "prefix2/"] + }, { "arn:aws:iam::123456789012:role/principal2" = [""] - } + }] privileged_principal_actions = [ "s3:PutObject", "s3:PutObjectAcl", diff --git a/docs/terraform.md b/docs/terraform.md index 63d2ea47..b7bac227 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -54,8 +54,7 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [acl](#input\_acl) | The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. | `string` | `"private"` | no | -| [acl\_grants](#input\_acl\_grants) | A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permission = string
uri = string
}))
| `null` | no | +| [acl](#input\_acl) | The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply.
We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. | `string` | `"private"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [allow\_encrypted\_uploads\_only](#input\_allow\_encrypted\_uploads\_only) | Set to `true` to prevent uploads of unencrypted objects to S3 bucket | `bool` | `false` | no | | [allow\_ssl\_requests\_only](#input\_allow\_ssl\_requests\_only) | Set to `true` to require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests | `bool` | `false` | no | @@ -71,8 +70,8 @@ | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [force\_destroy](#input\_force\_destroy) | A boolean string 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 | -| [grants](#input\_grants) | DEPRECATED (replaced by `acl_grants`): A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permissions = list(string)
uri = string
}))
| `null` | no | +| [force\_destroy](#input\_force\_destroy) | When `true`, permits a non-empty S3 bucket to be deleted by first deleting all objects in the bucket.
THESE OBJECTS ARE NOT RECOVERABLE even if they were versioned and stored in Glacier. | `bool` | `false` | no | +| [grants](#input\_grants) | A list of policy grants for the bucket, taking a list of permissions.
Conflicts with `acl`. Set `acl` to `null` to use this. |
list(object({
id = string
type = string
permissions = list(string)
uri = string
}))
| `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ignore\_public\_acls](#input\_ignore\_public\_acls) | Set to `false` to disable the ignoring of public access lists on the bucket | `bool` | `true` | no | | [kms\_master\_key\_arn](#input\_kms\_master\_key\_arn) | The AWS KMS master key ARN used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms` | `string` | `""` | no | @@ -80,24 +79,25 @@ | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [lifecycle\_configuration\_rules](#input\_lifecycle\_configuration\_rules) | A list of lifecycle rules |
list(object({
id = string
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
|
[
{
"abort_incomplete_multipart_upload_days": 90,
"deeparchive_transition_days": 90,
"enable_current_object_expiration": true,
"enable_deeparchive_transition": false,
"enable_glacier_transition": true,
"enable_noncurrent_version_expiration": true,
"enable_standard_ia_transition": false,
"enabled": false,
"expiration_days": 90,
"glacier_transition_days": 60,
"id": "noop",
"noncurrent_version_deeparchive_transition_days": 60,
"noncurrent_version_expiration_days": 90,
"noncurrent_version_glacier_transition_days": 30,
"prefix": "",
"standard_transition_days": 30,
"tags": {}
}
]
| no | -| [lifecycle\_rules](#input\_lifecycle\_rules) | DEPRECATED: A list of lifecycle rules |
list(object({
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
| `null` | no | +| [lifecycle\_configuration\_rules](#input\_lifecycle\_configuration\_rules) | A list of lifecycle V2 rules |
list(object({
enabled = bool
id = string

abort_incomplete_multipart_upload_days = number

# `filter_and` is the `and` configuration block inside the `filter` configuration.
# This is the only place you should specify a prefix.
filter_and = any
expiration = any
transition = list(any)

noncurrent_version_expiration = any
noncurrent_version_transition = list(any)
}))
| `[]` | no | +| [lifecycle\_rule\_ids](#input\_lifecycle\_rule\_ids) | DEPRECATED (use `lifecycle_configuration_rules`): A list of IDs to assign to corresponding `lifecycle_rules` | `list(string)` | `[]` | no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | DEPRECATED (`use lifecycle_configuration_rules`): A list of lifecycle rules |
list(object({
prefix = string
enabled = bool
tags = map(string)

enable_glacier_transition = bool
enable_deeparchive_transition = bool
enable_standard_ia_transition = bool
enable_current_object_expiration = bool
enable_noncurrent_version_expiration = bool

abort_incomplete_multipart_upload_days = number
noncurrent_version_glacier_transition_days = number
noncurrent_version_deeparchive_transition_days = number
noncurrent_version_expiration_days = number

standard_transition_days = number
glacier_transition_days = number
deeparchive_transition_days = number
expiration_days = number
}))
| `null` | no | | [logging](#input\_logging) | Bucket access logging configuration. |
object({
bucket_name = string
prefix = string
})
| `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [object\_lock\_configuration](#input\_object\_lock\_configuration) | A configuration for S3 object locking. With S3 Object Lock, you can store objects using a `write once, read many` (WORM) model. Object Lock can help prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. |
object({
mode = string # Valid values are GOVERNANCE and COMPLIANCE.
days = number
years = number
})
| `null` | no | -| [policy](#input\_policy) | DEPRECATED: A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no | +| [policy](#input\_policy) | DEPRECATED (use `source_policy_documents`): A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy | `string` | `""` | no | | [privileged\_principal\_actions](#input\_privileged\_principal\_actions) | List of actions to permit `privileged_principal_arns` to perform on bucket and bucket prefixes (see `privileged_principal_arns`) | `list(string)` | `[]` | no | -| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions.
Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'. | `map(list(string))` | `{}` | no | +| [privileged\_principal\_arns](#input\_privileged\_principal\_arns) | List of maps. Each map has one key, an IAM Principal ARN, whose associated value is
a list of S3 path prefixes to grant `privileged_principal_actions` permissions for that principal,
in addition to the bucket itself, which is automatically included. Prefixes should not begin with '/'. | `list(map(list(string)))` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [replication\_rules](#input\_replication\_rules) | DEPRECATED: Use s3\_replication\_rules instead. | `list(any)` | `null` | no | +| [replication\_rules](#input\_replication\_rules) | DEPRECATED (use `s3_replication_rules`): Specifies the replication rules for S3 bucket replication if enabled. You must also set s3\_replication\_enabled to true. | `list(any)` | `null` | no | | [restrict\_public\_buckets](#input\_restrict\_public\_buckets) | Set to `false` to disable the restricting of making the bucket public | `bool` | `true` | no | | [s3\_object\_ownership](#input\_s3\_object\_ownership) | Specifies the S3 object ownership control. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'. | `string` | `"ObjectWriter"` | no | | [s3\_replica\_bucket\_arn](#input\_s3\_replica\_bucket\_arn) | A single S3 bucket ARN to use for all replication rules.
Note: The destination bucket can be specified in the replication rule itself
(which allows for multiple destinations), in which case it will take precedence over this variable. | `string` | `""` | no | | [s3\_replication\_enabled](#input\_s3\_replication\_enabled) | Set this to true and specify `s3_replication_rules` to enable replication. `versioning_enabled` must also be `true`. | `bool` | `false` | no | | [s3\_replication\_rules](#input\_s3\_replication\_rules) | Specifies the replication rules for S3 bucket replication if enabled. You must also set s3\_replication\_enabled to true. | `list(any)` | `null` | no | | [s3\_replication\_source\_roles](#input\_s3\_replication\_source\_roles) | Cross-account IAM Role ARNs that will be allowed to perform S3 replication to this bucket (for replication within the same AWS account, it's not necessary to adjust the bucket policy). | `list(string)` | `[]` | no | -| [source\_policy\_documents](#input\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements defined in source\_policy\_documents or source\_json must have unique sids. Statements with the same sid from documents assigned to the override\_json and override\_policy\_documents arguments will override source statements. | `list(string)` | `[]` | no | +| [source\_policy\_documents](#input\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document.
Statements defined in source\_policy\_documents or source\_json must have unique SIDs.
Statement having SIDs that match policy SIDs generated by this module will override them. | `list(string)` | `[]` | no | | [sse\_algorithm](#input\_sse\_algorithm) | The server-side encryption algorithm to use. Valid values are `AES256` and `aws:kms` | `string` | `"AES256"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/examples/complete/lifecycle.us-east-2.tfvars b/examples/complete/lifecycle.us-east-2.tfvars index 00deda0f..488d52e9 100644 --- a/examples/complete/lifecycle.us-east-2.tfvars +++ b/examples/complete/lifecycle.us-east-2.tfvars @@ -8,6 +8,37 @@ name = "s3-lifecycle-test" acl = "private" +lifecycle_configuration_rules = [ + { + enabled = true # bool + id = "v2rule" + + abort_incomplete_multipart_upload_days = 1 # number + + filter_and = null + expiration = { + days = 120 # integer > 0 + } + noncurrent_version_expiration = { + newer_noncurrent_versions = 3 # integer > 0 + noncurrent_days = 60 # integer >= 0 + } + transition = [{ + days = 30 # integer >= 0 + storage_class = "STANDARD_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }, + { + days = 60 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + noncurrent_version_transition = [{ + newer_noncurrent_versions = 3 # integer >= 0 + noncurrent_days = 30 # integer >= 0 + storage_class = "ONEZONE_IA" # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + } +] + lifecycle_rules = [ { prefix = null @@ -36,20 +67,20 @@ lifecycle_rules = [ tags = {} enable_glacier_transition = false - enable_deeparchive_transition = false + enable_deeparchive_transition = true enable_standard_ia_transition = false enable_current_object_expiration = true enable_noncurrent_version_expiration = true abort_incomplete_multipart_upload_days = 1 noncurrent_version_glacier_transition_days = 0 - noncurrent_version_deeparchive_transition_days = 0 - noncurrent_version_expiration_days = 30 + noncurrent_version_deeparchive_transition_days = 120 + noncurrent_version_expiration_days = 366 standard_transition_days = 0 glacier_transition_days = 0 - deeparchive_transition_days = 0 - expiration_days = 30 + deeparchive_transition_days = 366 + expiration_days = 366 * 4 } ] diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 0f70e147..779bd4c3 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -10,6 +10,7 @@ module "s3_bucket" { force_destroy = var.force_destroy grants = var.grants lifecycle_rules = var.lifecycle_rules + lifecycle_configuration_rules = var.lifecycle_configuration_rules versioning_enabled = var.versioning_enabled allow_encrypted_uploads_only = var.allow_encrypted_uploads_only allowed_bucket_actions = var.allowed_bucket_actions diff --git a/examples/complete/privileged-principals.tf b/examples/complete/privileged-principals.tf index bc40c97b..663f8b75 100644 --- a/examples/complete/privileged-principals.tf +++ b/examples/complete/privileged-principals.tf @@ -1,15 +1,16 @@ locals { account_id = data.aws_caller_identity.current.account_id - principal_names = [ - "arn:aws:iam::${local.account_id}:role/${join("", module.deployment_principal_label.*.id)}", - "arn:aws:iam::${local.account_id}:role/${join("", module.additional_deployment_principal_label.*.id)}" + # Must use derived values in order to validate `count` clauses + privileged_principal_arns = var.privileged_principal_enabled == false ? [] : [ + { + (aws_iam_role.deployment_iam_role[0].arn) = [""] + }, + { + (aws_iam_role.additional_deployment_iam_role[0].arn) = ["prefix1/", "prefix2/"] + } ] - privileged_principal_arns = var.privileged_principal_enabled ? { - (local.principal_names[0]) = [""] - (local.principal_names[1]) = ["prefix1/", "prefix2/"] - } : {} } data "aws_caller_identity" "current" {} diff --git a/examples/complete/variables.tf b/examples/complete/variables.tf index 3520e45f..d72edf76 100644 --- a/examples/complete/variables.tf +++ b/examples/complete/variables.tf @@ -63,6 +63,26 @@ variable "lifecycle_rules" { description = "A list of lifecycle rules." } +variable "lifecycle_configuration_rules" { + type = list(object({ + enabled = bool + id = string + + abort_incomplete_multipart_upload_days = number + + # `filter_and` is the `and` configuration block inside the `filter` configuration. + # This is the only place you should specify a prefix. + filter_and = any + expiration = any + transition = list(any) + + noncurrent_version_expiration = any + noncurrent_version_transition = list(any) + })) + default = [] + description = "A list of lifecycle V2 rules" +} + variable "s3_replication_enabled" { type = bool default = false diff --git a/lifecycle.tf b/lifecycle.tf new file mode 100644 index 00000000..5fcee2b7 --- /dev/null +++ b/lifecycle.tf @@ -0,0 +1,223 @@ +locals { + # full_lifecycle_rule_schema is just for documentation, not actually used. + full_lifecycle_rule_schema = { + enabled = true # bool + id = null # string, must be specified and unique + + abort_incomplete_multipart_upload_days = null # number + filter_and = { + object_size_greater_than = null # integer >= 0 + object_size_less_than = null # integer >= 1 + prefix = null # string + tags = {} # map(string) + } + expiration = { + date = null # string + days = null # integer > 0 + expired_object_delete_marker = null # bool + } + noncurrent_version_expiration = { + newer_noncurrent_versions = null # integer > 0 + noncurrent_days = null # integer >= 0 + } + transition = [{ + date = null # string + days = null # integer >= 0 + storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + noncurrent_version_transition = [{ + newer_noncurrent_versions = null # integer >= 0 + noncurrent_days = null # integer >= 0 + storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + } + + lifecycle_configuration_rules = var.lifecycle_configuration_rules == null ? [] : var.lifecycle_configuration_rules + # Normalize the input, filling in missing fields + normalized_lifecycle_configuration_rules = [for rule in local.lifecycle_configuration_rules : { + enabled = rule.enabled + id = rule.id + + abort_incomplete_multipart_upload_days = rule.abort_incomplete_multipart_upload_days # number + filter_and = (try(rule.filter_and.object_size_greater_than, null) == null && + try(rule.filter_and.object_size_less_than, null) == null && + try(length(rule.filter_and.prefix), 0) == 0 && + try(length(rule.filter_and.tags), 0) == 0) ? null : { + object_size_greater_than = try(rule.filter_and.object_size_greater_than, null) + object_size_less_than = try(rule.filter_and.object_size_less_than, null) + prefix = try(length(rule.filter_and.prefix), 0) == 0 ? null : rule.filter_and.prefix + tags = try(length(rule.filter_and.tags), 0) == 0 ? {} : rule.filter_and.tags + } + # We use "!= true" because it covers !null as well as !false, and allows the "null" option to be on the same line. + expiration = (try(rule.expiration.date, null) == null && + try(rule.expiration.days, null) == null && + try(rule.expiration.expired_object_delete_marker, null) == null) ? null : { + date = try(rule.expiration.date, null) + days = try(rule.expiration.days, null) + + expired_object_delete_marker = try(rule.expiration.expired_object_delete_marker, null) + } + noncurrent_version_expiration = (try(rule.noncurrent_version_expiration.noncurrent_days, null) == null && + try(rule.noncurrent_version_expiration.newer_noncurrent_versions, null) == null) ? null : { + newer_noncurrent_versions = try(rule.noncurrent_version_expiration.newer_noncurrent_versions, null) + noncurrent_days = try(rule.noncurrent_version_expiration.noncurrent_days, null) + } + transition = rule.transition == null ? [] : [for t in rule.transition : { + date = try(t.date, null) + days = try(t.days, null) + storage_class = t.storage_class + } if try(t.date, null) != null || try(t.days, null) != null] + noncurrent_version_transition = rule.noncurrent_version_transition == null ? [] : [ + for t in rule.noncurrent_version_transition : + { + newer_noncurrent_versions = try(t.newer_noncurrent_versions, null) + noncurrent_days = try(t.noncurrent_days, null) + storage_class = t.storage_class + } if try(t.newer_noncurrent_versions, null) != null || try(t.noncurrent_days, null) != null + ] + }] + + # Backwards compatibility: translate old `lifecycle_rules` variable to new format + lifecycle_rules = var.lifecycle_rules == null ? [] : var.lifecycle_rules + normalized_lifecycle_rules = [for i, rule in local.lifecycle_rules : { + enabled = rule.enabled + id = try(var.lifecycle_rule_ids[i], "rule-${i + 1}") + + abort_incomplete_multipart_upload_days = rule.abort_incomplete_multipart_upload_days # number + filter_and = try(length(rule.prefix), 0) == 0 && try(length(rule.tags), 0) == 0 ? null : { + object_size_greater_than = null # integer >= 0 + object_size_less_than = null # integer >= 1 + prefix = rule.prefix == "" ? null : rule.prefix # string + tags = rule.tags == null ? {} : rule.tags # map(string) + } + # We use "!= true" because it covers !null as well as !false, and allows the "null" option to be on the same line. + expiration = rule.enable_current_object_expiration != true ? null : { + date = null # string + days = rule.expiration_days # integer > 0 + expired_object_delete_marker = null # bool + } + noncurrent_version_expiration = rule.enable_noncurrent_version_expiration != true ? null : { + newer_noncurrent_versions = null # integer > 0 + noncurrent_days = rule.noncurrent_version_expiration_days # integer >= 0 + } + transition = concat( + rule.enable_standard_ia_transition != true ? [] : + [{ + date = null # string + days = rule.standard_transition_days # integer >= 0 + storage_class = "STANDARD_IA" + }], + rule.enable_glacier_transition != true ? [] : + [{ + date = null # string + days = rule.glacier_transition_days # integer >= 0 + storage_class = "GLACIER" + }], + rule.enable_deeparchive_transition != true ? [] : + [{ + date = null # string + days = rule.deeparchive_transition_days # integer >= 0 + storage_class = "DEEP_ARCHIVE" + }], + ) + noncurrent_version_transition = concat( + # In the old `lifecycle_rules` variable, `enable_glacier_transition` + # enabled the transition for both current and non-current version. + rule.enable_glacier_transition != true ? [] : + [{ + newer_noncurrent_versions = null # integer >= 0 + noncurrent_days = rule.noncurrent_version_glacier_transition_days # integer >= 0 + storage_class = "GLACIER" + }], + # In the old `lifecycle_rules` variable, `enable_deeparchive_transition` + # enabled the transition for both current and non-current version. + rule.enable_deeparchive_transition != true ? [] : + [{ + newer_noncurrent_versions = null # string + noncurrent_days = rule.noncurrent_version_deeparchive_transition_days # integer >= 0 + storage_class = "DEEP_ARCHIVE" + }], + ) + }] + + lc_rules = concat(local.normalized_lifecycle_rules, local.normalized_lifecycle_configuration_rules) +} + + +resource "aws_s3_bucket_lifecycle_configuration" "default" { + count = local.enabled && length(local.lc_rules) > 0 ? 1 : 0 + bucket = join("", aws_s3_bucket.default.*.id) + + dynamic "rule" { + for_each = local.lc_rules + + content { + id = rule.value.id + status = rule.value.enabled == true ? "Enabled" : "Disabled" + + # Filter is always required due to https://github.com/hashicorp/terraform-provider-aws/issues/23299 + filter { + dynamic "and" { + for_each = rule.value.filter_and == null ? [] : [rule.value.filter_and] + content { + object_size_greater_than = and.value.object_size_greater_than + object_size_less_than = and.value.object_size_less_than + prefix = and.value.prefix + tags = and.value.tags + } + } + } + + dynamic "abort_incomplete_multipart_upload" { + for_each = rule.value.abort_incomplete_multipart_upload_days == null ? [] : [1] + content { + days_after_initiation = rule.value.abort_incomplete_multipart_upload_days + } + } + + dynamic "expiration" { + for_each = rule.value.expiration == null ? [] : [rule.value.expiration] + content { + date = expiration.value.date + days = expiration.value.days + expired_object_delete_marker = expiration.value.expired_object_delete_marker + } + } + + dynamic "noncurrent_version_expiration" { + for_each = rule.value.noncurrent_version_expiration == null ? [] : [rule.value.noncurrent_version_expiration] + iterator = expiration + content { + newer_noncurrent_versions = expiration.value.newer_noncurrent_versions + noncurrent_days = expiration.value.noncurrent_days + } + } + + dynamic "transition" { + for_each = rule.value.transition + + content { + date = transition.value.date + days = transition.value.days + storage_class = transition.value.storage_class + } + } + + dynamic "noncurrent_version_transition" { + for_each = rule.value.noncurrent_version_transition + iterator = transition + content { + newer_noncurrent_versions = transition.value.newer_noncurrent_versions + noncurrent_days = transition.value.noncurrent_days + storage_class = transition.value.storage_class + } + } + } + } + + depends_on = [ + # versioning must be set before lifecycle configuration + aws_s3_bucket_versioning.default + ] +} + diff --git a/main.tf b/main.tf index 7c890fa6..72a067b6 100644 --- a/main.tf +++ b/main.tf @@ -1,20 +1,32 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + partition = join("", data.aws_partition.current.*.partition) replication_enabled = local.enabled && var.s3_replication_enabled versioning_enabled = local.enabled && var.versioning_enabled transfer_acceleration_enabled = local.enabled && var.transfer_acceleration_enabled bucket_name = var.bucket_name != null && var.bucket_name != "" ? var.bucket_name : module.this.id - bucket_arn = "arn:${data.aws_partition.current.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}" - - # Deprecate `replication_rules` in favor of `s3_replication_rules` to keep all the replication related - # inputs grouped under s3_replica[tion] - s3_replication_rules = var.replication_rules == null ? var.s3_replication_rules : var.replication_rules + bucket_arn = "arn:${local.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}" public_access_block_enabled = var.block_public_acls || var.block_public_policy || var.ignore_public_acls || var.restrict_public_buckets + + acl_grants = var.grants == null ? [] : flatten( + [ + for g in var.grants : [ + for p in g.permissions : { + id = g.id + type = g.type + permission = p + uri = g.uri + } + ] + ]) } +data "aws_partition" "current" { count = local.enabled ? 1 : 0 } +data "aws_canonical_user_id" "default" { count = local.enabled ? 1 : 0 } + resource "aws_s3_bucket" "default" { #bridgecrew:skip=BC_AWS_S3_13:Skipping `Enable S3 Bucket Logging` because we do not have good defaults #bridgecrew:skip=CKV_AWS_52:Skipping `Ensure S3 bucket has MFA delete enabled` due to issue in terraform (https://github.com/hashicorp/terraform-provider-aws/issues/629). @@ -53,104 +65,6 @@ resource "aws_s3_bucket_versioning" "default" { } } -resource "aws_s3_bucket_lifecycle_configuration" "default" { - count = local.enabled && length(local.lifecycle_configuration_rules) > 0 ? 1 : 0 - bucket = join("", aws_s3_bucket.default.*.id) - - dynamic "rule" { - for_each = local.lifecycle_configuration_rules - - content { - id = rule.value.id - status = rule.value.enabled == true ? "Enabled" : "Disabled" - - # Filter is always required due to https://github.com/hashicorp/terraform-provider-aws/issues/23299 - filter { - dynamic "and" { - for_each = (try(length(rule.value.prefix), 0) + try(length(rule.value.tags), 0)) > 0 ? [1] : [] - content { - prefix = rule.value.prefix == null ? "" : rule.value.prefix - tags = try(length(rule.value.tags), 0) > 0 ? rule.value.tags : {} - } - } - } - - dynamic "abort_incomplete_multipart_upload" { - for_each = try(tonumber(rule.value.abort_incomplete_multipart_upload_days), null) != null ? [1] : [] - content { - days_after_initiation = rule.value.abort_incomplete_multipart_upload_days - } - } - - dynamic "noncurrent_version_expiration" { - for_each = rule.value.enable_noncurrent_version_expiration ? [1] : [] - - content { - noncurrent_days = rule.value.noncurrent_version_expiration_days - } - } - - dynamic "noncurrent_version_transition" { - for_each = rule.value.enable_glacier_transition ? [1] : [] - - content { - noncurrent_days = rule.value.noncurrent_version_glacier_transition_days - storage_class = "GLACIER" - } - } - - dynamic "noncurrent_version_transition" { - for_each = rule.value.enable_deeparchive_transition ? [1] : [] - - content { - noncurrent_days = rule.value.noncurrent_version_deeparchive_transition_days - storage_class = "DEEP_ARCHIVE" - } - } - - dynamic "transition" { - for_each = rule.value.enable_glacier_transition ? [1] : [] - - content { - days = rule.value.glacier_transition_days - storage_class = "GLACIER" - } - } - - dynamic "transition" { - for_each = rule.value.enable_deeparchive_transition ? [1] : [] - - content { - days = rule.value.deeparchive_transition_days - storage_class = "DEEP_ARCHIVE" - } - } - - dynamic "transition" { - for_each = rule.value.enable_standard_ia_transition ? [1] : [] - - content { - days = rule.value.standard_transition_days - storage_class = "STANDARD_IA" - } - } - - dynamic "expiration" { - for_each = rule.value.enable_current_object_expiration ? [1] : [] - - content { - days = rule.value.expiration_days - } - } - } - } - - depends_on = [ - # versioning must be set before lifecycle configuration - aws_s3_bucket_versioning.default - ] -} - resource "aws_s3_bucket_logging" "default" { count = local.enabled && var.logging != null ? 1 : 0 bucket = join("", aws_s3_bucket.default.*.id) @@ -231,10 +145,6 @@ resource "aws_s3_bucket_cors_configuration" "default" { } } -data "aws_canonical_user_id" "default" { - count = local.enabled ? 1 : 0 -} - resource "aws_s3_bucket_acl" "default" { count = local.enabled ? 1 : 0 bucket = join("", aws_s3_bucket.default.*.id) @@ -405,8 +315,6 @@ module "s3_user" { context = module.this.context } -data "aws_partition" "current" {} - data "aws_iam_policy_document" "bucket_policy" { count = local.enabled ? 1 : 0 @@ -511,18 +419,18 @@ data "aws_iam_policy_document" "bucket_policy" { } dynamic "statement" { - for_each = keys(var.privileged_principal_arns) + for_each = var.privileged_principal_arns content { sid = "AllowPrivilegedPrincipal[${statement.key}]" # add indices to Sid actions = var.privileged_principal_actions resources = distinct(flatten([ - "arn:${data.aws_partition.current.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}", - formatlist("arn:${data.aws_partition.current.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}/%s*", var.privileged_principal_arns[statement.value]), + "arn:${local.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}", + formatlist("arn:${local.partition}:s3:::${join("", aws_s3_bucket.default.*.id)}/%s*", values(statement.value)[0]), ])) principals { type = "AWS" - identifiers = [statement.value] + identifiers = [keys(statement.value)[0]] } } } @@ -531,8 +439,8 @@ data "aws_iam_policy_document" "bucket_policy" { data "aws_iam_policy_document" "aggregated_policy" { count = local.enabled ? 1 : 0 - source_policy_documents = compact(concat([var.policy], var.source_policy_documents)) - override_policy_documents = data.aws_iam_policy_document.bucket_policy.*.json + source_policy_documents = data.aws_iam_policy_document.bucket_policy.*.json + override_policy_documents = local.source_policy_documents } resource "aws_s3_bucket_policy" "default" { diff --git a/test/src/go.sum b/test/src/go.sum index 87ae8508..e550698b 100644 --- a/test/src/go.sum +++ b/test/src/go.sum @@ -98,7 +98,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= @@ -480,7 +479,6 @@ github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.9.1 h1:eOy4gREY0/ZQHNItlfuEZqtcQbXIxzojlP301hDpnac= github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= diff --git a/variables-deprecated.tf b/variables-deprecated.tf index 71a0772a..cee66689 100644 --- a/variables-deprecated.tf +++ b/variables-deprecated.tf @@ -1,27 +1,7 @@ -variable "grants" { - type = list(object({ - id = string - type = string - permissions = list(string) - uri = string - })) - default = null - - description = "DEPRECATED (replaced by `acl_grants`): A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this." -} - -locals { - acl_grants = var.grants == null ? var.acl_grants : flatten( - [ - for g in var.grants : [ - for p in g.permissions : { - id = g.id - type = g.type - permission = p - uri = g.uri - } - ] - ]) +variable "lifecycle_rule_ids" { + type = list(string) + default = [] + description = "DEPRECATED (use `lifecycle_configuration_rules`): A list of IDs to assign to corresponding `lifecycle_rules`" } variable "lifecycle_rules" { @@ -47,32 +27,17 @@ variable "lifecycle_rules" { expiration_days = number })) default = null - description = "DEPRECATED: A list of lifecycle rules" + description = "DEPRECATED (`use lifecycle_configuration_rules`): A list of lifecycle rules" } -locals { - lifecycle_configuration_rules = var.lifecycle_rules == null ? var.lifecycle_configuration_rules : ( - [for i, v in var.lifecycle_rules : { - id = "rule-${i + 1}" - prefix = v.prefix - enabled = v.enabled - tags = v.tags - - enable_glacier_transition = v.enable_glacier_transition - enable_deeparchive_transition = v.enable_deeparchive_transition - enable_standard_ia_transition = v.enable_standard_ia_transition - enable_current_object_expiration = v.enable_current_object_expiration - enable_noncurrent_version_expiration = v.enable_noncurrent_version_expiration - - abort_incomplete_multipart_upload_days = v.abort_incomplete_multipart_upload_days - noncurrent_version_glacier_transition_days = v.noncurrent_version_glacier_transition_days - noncurrent_version_deeparchive_transition_days = v.noncurrent_version_deeparchive_transition_days - noncurrent_version_expiration_days = v.noncurrent_version_expiration_days +variable "policy" { + type = string + default = "" + description = "DEPRECATED (use `source_policy_documents`): A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy" +} - standard_transition_days = v.standard_transition_days - glacier_transition_days = v.glacier_transition_days - deeparchive_transition_days = v.deeparchive_transition_days - expiration_days = v.expiration_days - }] - ) -} \ No newline at end of file +variable "replication_rules" { + type = list(any) + default = null + description = "DEPRECATED (use `s3_replication_rules`): Specifies the replication rules for S3 bucket replication if enabled. You must also set s3_replication_enabled to true." +} diff --git a/variables.tf b/variables.tf index 4a6fde88..c8a41801 100644 --- a/variables.tf +++ b/variables.tf @@ -1,37 +1,47 @@ variable "acl" { type = string default = "private" - description = "The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`." + description = <<-EOT + The [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. + We recommend `private` to avoid exposing sensitive information. Conflicts with `grants`. + EOT } -variable "acl_grants" { +variable "grants" { type = list(object({ - id = string - type = string - permission = string - uri = string + id = string + type = string + permissions = list(string) + uri = string })) - default = null - - description = "A list of policy grants for the bucket. Conflicts with `acl`. Set `acl` to `null` to use this." -} + default = [] -variable "policy" { - type = string - default = "" - description = "DEPRECATED: A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy" + description = <<-EOT + A list of policy grants for the bucket, taking a list of permissions. + Conflicts with `acl`. Set `acl` to `null` to use this. + EOT } variable "source_policy_documents" { type = list(string) default = [] - description = "List of IAM policy documents that are merged together into the exported document. Statements defined in source_policy_documents or source_json must have unique sids. Statements with the same sid from documents assigned to the override_json and override_policy_documents arguments will override source statements." + description = <<-EOT + List of IAM policy documents that are merged together into the exported document. + Statements defined in source_policy_documents or source_json must have unique SIDs. + Statement having SIDs that match policy SIDs generated by this module will override them. + EOT +} +locals { + source_policy_documents = compact(concat([var.policy], var.source_policy_documents)) } variable "force_destroy" { type = bool default = false - description = "A boolean string that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable" + description = <<-EOT + When `true`, permits a non-empty S3 bucket to be deleted by first deleting all objects in the bucket. + THESE OBJECTS ARE NOT RECOVERABLE even if they were versioned and stored in Glacier. + EOT } variable "versioning_enabled" { @@ -85,53 +95,65 @@ variable "allow_ssl_requests_only" { description = "Set to `true` to require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests" } +/* +Schema for lifecycle_configuration_rules +{ + enabled = true # bool + id = string + + abort_incomplete_multipart_upload_days = null # number + + filter_and = { + object_size_greater_than = null # integer >= 0 + object_size_less_than = null # integer >= 1 + prefix = null # string + tags = {} # map(string) + } + expiration = { + date = null # string, RFC3339 time format, GMT + days = null # integer > 0 + expired_object_delete_marker = null # bool + } + noncurrent_version_expiration = { + newer_noncurrent_versions = null # integer > 0 + noncurrent_days = null # integer >= 0 + } + transition = [{ + date = null # string, RFC3339 time format, GMT + days = null # integer >= 0 + storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] + noncurrent_version_transition = [{ + newer_noncurrent_versions = null # integer >= 0 + noncurrent_days = null # integer >= 0 + storage_class = null # string/enum, one of GLACIER, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, DEEP_ARCHIVE, GLACIER_IR. + }] +} + +We only partly specify the object to allow for compatible future extension. +*/ + variable "lifecycle_configuration_rules" { type = list(object({ - id = string - prefix = string enabled = bool - tags = map(string) - - enable_glacier_transition = bool - enable_deeparchive_transition = bool - enable_standard_ia_transition = bool - enable_current_object_expiration = bool - enable_noncurrent_version_expiration = bool - - abort_incomplete_multipart_upload_days = number - noncurrent_version_glacier_transition_days = number - noncurrent_version_deeparchive_transition_days = number - noncurrent_version_expiration_days = number - - standard_transition_days = number - glacier_transition_days = number - deeparchive_transition_days = number - expiration_days = number + id = string + + abort_incomplete_multipart_upload_days = number + + # `filter_and` is the `and` configuration block inside the `filter` configuration. + # This is the only place you should specify a prefix. + filter_and = any + expiration = any + transition = list(any) + + noncurrent_version_expiration = any + noncurrent_version_transition = list(any) })) - default = [{ - id = "noop" - enabled = false - prefix = "" - tags = {} - - enable_glacier_transition = true - enable_deeparchive_transition = false - enable_standard_ia_transition = false - enable_current_object_expiration = true - enable_noncurrent_version_expiration = true - - abort_incomplete_multipart_upload_days = 90 - noncurrent_version_glacier_transition_days = 30 - noncurrent_version_deeparchive_transition_days = 60 - noncurrent_version_expiration_days = 90 - - standard_transition_days = 30 - glacier_transition_days = 60 - deeparchive_transition_days = 90 - expiration_days = 90 - }] - description = "A list of lifecycle rules" + default = [] + description = "A list of lifecycle V2 rules" } +# See lifecycle.tf for conversion of deprecated `lifecyle_rules` to `lifecycle_configuration_rules` + variable "cors_rule_inputs" { type = list(object({ @@ -224,11 +246,10 @@ variable "s3_replication_rules" { default = null description = "Specifies the replication rules for S3 bucket replication if enabled. You must also set s3_replication_enabled to true." } - -variable "replication_rules" { - type = list(any) - default = null - description = "DEPRECATED: Use s3_replication_rules instead." +locals { + # Deprecate `replication_rules` in favor of `s3_replication_rules` to keep all the replication related + # inputs grouped under s3_replica[tion] + s3_replication_rules = var.replication_rules == null ? var.s3_replication_rules : var.replication_rules } variable "s3_replication_source_roles" { @@ -266,12 +287,17 @@ variable "website_inputs" { description = "Specifies the static website hosting configuration object." } +# Need input to be a list to fix https://github.com/cloudposse/terraform-aws-s3-bucket/issues/102 variable "privileged_principal_arns" { - type = map(list(string)) - default = {} + # type = map(list(string)) + # default = {} + type = list(map(list(string))) + default = [] + description = <<-EOT - (Optional) Map of IAM Principal ARNs to lists of S3 path prefixes to grant `privileged_principal_actions` permissions. - Resource list will include the bucket itself along with all the prefixes. Prefixes should not begin with '/'. + List of maps. Each map has one key, an IAM Principal ARN, whose associated value is + a list of S3 path prefixes to grant `privileged_principal_actions` permissions for that principal, + in addition to the bucket itself, which is automatically included. Prefixes should not begin with '/'. EOT }