compliance.tf

Terraform rejects both ways of passing prevent_destroy

Error: Reserved block type name in module block

The 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 demo

After 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.

On this page

Ask AI about this

Help improve this page