compliance.tf

S3 bucket: known Terraform limitations

Some things people ask the terraform-aws-modules/s3-bucket/aws module for cannot be implemented by any module, in any registry. They are limits of Terraform itself. This page collects the recurring ones for this module, says plainly what causes each, gives the native workaround in full, and - where one exists - shows the Operational Rule that removes the need for a fork.

This module is downloaded ~2.5M times a month, so these come up often.

Read the native option first

Every problem below names the workaround you can apply today without compliance.tf. Several of them are the right answer on their own. The rule is an alternative to maintaining a fork, not a replacement for a control AWS already offers you.


terraform destroy deleted a production S3 bucket

A terraform destroy in the wrong workspace, or a rename that Terraform reads as a replacement, deletes the bucket this module manages. prevent_destroy is the built-in guard against exactly that, and there is no module input for it.

Why Terraform cannot fix this

lifecycle is a meta-argument block, and Terraform evaluates it before it evaluates the rest of the configuration. Its arguments therefore cannot reference a variable, a local, or anything else that is computed. That is the whole reason no module can expose prevent_destroy, ignore_changes, or create_before_destroy as an input: there is no expression a module author could put there that Terraform would accept.

This is a property of Terraform, not of this module. The request to lift the restriction has been open since 2015, it is one of the most-supported requests in the tracker, and OpenTofu carries the same request. Until one of them ships a way to set a meta-argument from outside the resource block, every module in every registry has the same limitation.

RepositoryIssueTitle
hashicorp/terraform#3116Cannot use interpolations in lifecycle attributes
hashicorp/terraform#18367Feature request: support prevent_destroy for modules
hashicorp/terraform#21546Passing ignore_changes into a module
hashicorp/terraform#24188Support for dynamic blocks and meta-arguments
hashicorp/terraform#27360A method to override configuration and meta arguments within a module
opentofu/opentofu#1329Support variables in lifecycle blocks

The native workaround

Fork this module and add the lifecycle block to the resource yourself. That works, and it is the honest answer - it is what module maintainers do when they need it. The cost is ongoing rather than one-off: the fork has to be re-synced with every upstream release, and each sync needs a review to confirm the block still lands on the resource it was meant for. A wrapper module does not avoid this, because the lifecycle block still has to sit inside the resource block, which is inside the module you did not write.

Two partial native options are worth knowing. An S3 bucket policy with an explicit Deny on s3:DeleteBucket stops the delete at the API, though it also has to be lifted by hand when you do want the bucket gone. And a review checklist on the plan output catches it when someone reads the plan, which is the failure mode the guard exists for.

The rule that answers it

lifecycle_prevent_destroy_data adds the block for you, server-side, while the module is being downloaded. What arrives in .terraform/modules/ is ordinary HCL with this change already in it:

 resource "aws_s3_bucket" "this" {
   # ... the module's own configuration, unchanged ...
+
+  lifecycle {
+    prevent_destroy = true
+  }
 }

Nothing else about the module changes: same inputs, same outputs, same version. The full definition is published at lifecycle_prevent_destroy_data.


Terraform keeps reverting tags on my S3 buckets

Cost-allocation tooling, a CSPM scanner, or an AWS Config remediation rule writes its own tag onto the bucket. Terraform sees the extra tag as drift and removes it on the next apply, and the external tool writes it back, indefinitely.

Why Terraform cannot fix this

Terraform's model is that it owns every attribute it can see. When a controller, an autoscaler, a scanner, or a deployment pipeline writes to one of those attributes, Terraform reads the new value as drift and plans to put its own value back. Both systems are behaving correctly; they simply disagree about who owns the field.

The only mechanism Terraform offers for splitting ownership is ignore_changes, and that is a lifecycle argument, which brings the literal-value restriction above with it. A module cannot accept the list of attributes to ignore as an input, so a module consumer has no way to declare the split.

RepositoryIssueTitle
hashicorp/terraform#27360A method to override configuration and meta arguments within a module
hashicorp/terraform#24188Support for dynamic blocks and meta-arguments
hashicorp/terraform-provider-aws#19583Provider produced inconsistent final plan / an invalid new value for .tags_all

The native workaround

Take the tag back into Terraform first, if you can. default_tags on the AWS provider applies a tag set to every resource the provider manages, and it removes the disagreement outright when the external writer can be turned off.

When the external writer cannot be turned off - a Kubernetes or load balancer controller writing its own tags is the usual case - the only native option left is ignore_changes = [tags, tags_all] inside the resource block, and that means forking the module. Fork this module and add the lifecycle block to the resource yourself. That works, and it is the honest answer - it is what module maintainers do when they need it. The cost is ongoing rather than one-off: the fork has to be re-synced with every upstream release, and each sync needs a review to confirm the block still lands on the resource it was meant for. A wrapper module does not avoid this, because the lifecycle block still has to sit inside the resource block, which is inside the module you did not write.

The rule that answers it

lifecycle_ignore_tags adds the block for you, server-side, while the module is being downloaded. What arrives in .terraform/modules/ is ordinary HCL with this change already in it:

 resource "aws_s3_bucket" "this" {
   # ... the module's own configuration, unchanged ...
+
+  lifecycle {
+    ignore_changes = [tags, tags_all]
+  }
 }

Nothing else about the module changes: same inputs, same outputs, same version. The full definition is published at lifecycle_ignore_tags.


Using the maintained alternative

compliance.tf serves this module from a registry that applies the rules above during terraform init. The change is the source line plus the rules you want; the inputs and outputs are the upstream module's.

module "s3_bucket" {
  source = "https://cis.compliance.tf/terraform-aws-modules/s3-bucket/aws?add_rules=lifecycle_ignore_tags,lifecycle_prevent_destroy_data"

  # ... the same inputs you pass today ...
}

On a framework host you name the rules you want in ?add_rules=, on top of whatever Baseline your organization has Enforced; on an organization host that organization's configuration decides. A module compliance.tf builds for you carries a compliancetf-manifest.json naming every rule the build received, each with an outcome recording what it actually did, so the change is auditable rather than implicit.

Before changing a source line, run it: the Rules Playground opens on this module with these rules selected and shows the diff the registry would serve, without an account. The Playground takes its list as ?rules= because it previews rules on their own rather than against your Baseline; your source line above keeps ?add_rules=, which adds to that Baseline instead of replacing it.


On this page

Ask AI about this

Help improve this page