Terraform rejects both ways of passing prevent_destroy
Error: Reserved block type name in module blockThe bucket is inside terraform-aws-modules/s3-bucket. You want Terraform to
reject an accidental destruction plan, so you put a lifecycle block on the
module call. Terraform rejects the block before it can give you that protection.
Moving the block inside a resource and exposing prevent_destroy as a variable
fails too. These are two different restrictions. Understanding them tells you
where the setting has to live, and which approaches can actually put it there.
The module call cannot carry the lifecycle block
This configuration pins s3-bucket 5.15.4:
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "5.15.4"
bucket = "example"
lifecycle {
prevent_destroy = true
}
}The captured error says:
Error: Reserved block type name in module block
on main.tf line 9, in module "s3_bucket":
9: lifecycle {
The block type name "lifecycle" is reserved for use by Terraform in a future
version.A module call groups resources behind an interface. Adding a block to that call does not place it on the bucket resource inside the module. A wrapper that still calls the same upstream module has the same boundary.
A variable does not make prevent_destroy configurable
The second example isolates the lifecycle setting with terraform_data, so no
AWS provider or credentials are involved:
resource "terraform_data" "bucket" {
lifecycle {
prevent_destroy = var.prevent_destroy
}
}Terraform reports:
Error: Variables not allowed
on main.tf line 11, in resource "terraform_data" "bucket":
11: prevent_destroy = var.prevent_destroy
Variables may not be used here.The capture also includes Error: Unsuitable value type. Both examples were
captured with Terraform 1.16.3 on 2026-10-07; the
full error files
include their versions and dates.
Terraform constructs its dependency graph before it can evaluate arbitrary expressions for these settings, so it requires literal values. The lifecycle reference explains that timing. The request to allow lifecycle variables is also tracked in hashicorp/terraform#3116.
That still leaves choices. You can maintain a fork with the literal block, own the resource implementation yourself, or use a policy check to reject plans that your team considers unsafe. A policy check can stop a plan; it does not insert a lifecycle block into the module.
The downloaded resource can contain the literal
The public preview for lifecycle_prevent_destroy_data on s3-bucket 5.15.4
returns this change to aws_s3_bucket:
tags = var.tags
+
+ lifecycle {
+ prevent_destroy = true
+ }
}Now the setting is where Terraform expects it: a literal inside the resource. The rule edits the module before Terraform reads it. No lifecycle variable or module-call block is needed.
I built compliance.tf because requests for changes such as lifecycle settings kept leading to module forks. It serves terraform-aws-modules with controls and operational rules added at download time. This capture shows the resulting HCL; it does not show an authenticated plan or a destruction test in AWS. The S3 preview also omits submodules, so it is not evidence that every resource in the package receives protection.
prevent_destroy rejects destruction and replacement plans while the resource
configuration remains present. Remove that configuration, and its protection
is gone. It also does not prevent
someone deleting a bucket through the AWS console. Review those paths
separately from the lifecycle setting.
Ignoring drift means choosing who owns the value
The gallery also previews lifecycle_ignore_scaling_changes on autoscaling
9.2.1:
lifecycle {
create_before_destroy = true
- ignore_changes = [
- load_balancers,
- target_group_arns,
- ]
+ ignore_changes = [load_balancers, target_group_arns, desired_capacity]
}The rule preserves the existing lifecycle settings and adds desired_capacity
to the ignored attributes. That is useful when a scaling controller, rather
than Terraform, should own changes to desired capacity. It also prevents
Terraform correcting an unintended change to that value. Decide ownership
before enabling the rule.
The tag rule makes the same tradeoff for tags and tags_all on the ordinary
S3 bucket, and tags on the directory bucket. It is an ownership decision,
not a way to classify every tag change as safe.
A removed provisioner needs a replacement workflow
provisioner_remove_blocks on lambda 8.8.1 removes the package-building
local-exec block from package.tf. For a runner that disallows provisioners,
this gives you code without that block. It does not give you a built artifact.
Use create_package = false and provide a prebuilt package. Moving the build
out of Terraform is a workflow change that belongs in the same review as the
rule. The gallery captures the removal, not a successful Lambda deployment.
A successful request can still apply no rule
The fifth gallery response requests variable_allowed_regions for s3-bucket
5.15.4. It returns not_configured, 0 rules applied and no changed files. The
rule needs region parameters; this public request supplies none.
That response is worth keeping beside the applied rules. It demonstrates why you should check the outcome and diff rather than treating HTTP success as proof of enforcement. An empty diff is not an allowed-region restriction.
Reproduce the errors before adopting the rule
git clone https://github.com/antonbabenko/compliance.tf-demo
cd compliance.tf-demo/scenarios/03-operational-rules
make demoAfter cloning, make demo replays the errors and five preview responses with
make, bash and jq. No account, network or AWS credentials are needed for
the replay. The gallery was captured on 2026-10-07.
Run make problem with Terraform and network access to reproduce the errors.
Its Makefile tolerates validation failures because those failures are the
example; read the messages rather than interpreting a successful make exit
as valid HCL. make live makes fresh public preview requests.
Downloading a rewritten module is separate. The repository's with-rules/
example uses:
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.15.4&rules=lifecycle_prevent_destroy_data,lifecycle_ignore_tags"It needs a token in ~/.netrc and appropriate inputs for the SOC 2 host; see
Registry Endpoints. Shared
organization rules can instead be managed in a
Baseline.
If you need a lifecycle literal inside a module resource, choose a mechanism
that puts it there. If you only need to reject an unsafe plan, a plan policy may
be enough. In either case, review the intentional-replacement path before
relying on prevent_destroy.